% vim: ts=4 sts=4 sw=4 et tw=75

\chapter{Awk 语言}
\label{chap:the_awk_language}

\marginpar{21}
这一章解释---大部分都带有例子---组成一个 awk 程序所需要的构造要素.
由于这次要描述
的是整个语言, 所以材料会非常琐细, 我们建议读者只需要浏览一下即可, 当
需要时再回来查阅细节.

最简单的 awk 程序是一个由多个 \patact 语句构成的序列:
\begin{pattern}
\textit{pattern} \texttt{\{} \textit{action} \texttt{\}} \par
\textit{pattern} \texttt{\{} \textit{action} \texttt{\}} \par
...
\end{pattern}
在某些语句中, 模式可以不存在; 还有些语句, 动作及其包围它的花括号也可以不存
在. 如果程序经过 awk 检查后没有发现语法错误, 它就会每次读取一个输入行,
对读取到的每一行, 按顺序检查每一个模式. 对每一个与当前行匹配的模式, 对应
的动作就会执行. 一个缺失的模式匹配每一个输入行, 因此每一个不带有模式的动
作对每一个输入行都会执行. 只含有模式而没有动作的语句, 会打印每一个匹配模式
的输入行. 大部分情况下, 在这一章出现的术语 ``输入行'' 与 ``记录'' 被当作
一对同义词. 在 \ref{sec:input} 节, 我们会讨论多行记录, 多行记录指
由多行数据组成的单个记录.

本章的第一节详细讨论模式. 第二节通过描述表达式, 赋值语句与流程控制语句,
展开对动作的讨论. 剩下的小节包括函数定义, 输出, 输入, 以及 awk 调用其
他程序的方式. 大部分小节都包含对主要性质的总结.

\pseudosubsec{输入文件 \filename{countries}}

作为本章许多 awk 程序的输入数据, 我们将使用文件 \filename{countries}. 每一
行都包括一个国家的名字, 面积 (以千平方英里为单位), 人口 (以百万为单位), 以
及这个国家所在的大陆. 数据来源于 1984 年, 苏联被归到了亚洲. 在文件里, 四列
数据用制表符分隔, 用一个空格分隔 \texttt{North} (\texttt{South}) 与
\texttt{America}.

文件 \filename{countries} 包含下面几行:
\marginpar{22}
\begin{awkcode}
    USSR        8649    275     Asia
    Canada      3852    25      North America
    China       3705    1032    Asia
    USA         3615    237     North America
    Brazil      3286    134     South America
    India       1267    746     Asia
    Mexico      762     78      North America
    France      211     55      Europe
    Japan       144     120     Asia
    Germany     96      61      Europe
    England     94      56      Europe
\end{awkcode}

在这一章的剩下部分里, 如果没有显式给出输入数据, 默认将 \filename{countries}
作为输入.

\pseudosubsec{程序格式}

\patact 语句, 以及动作内的语句通常用换行符分隔, 但是若干条语句也可
以出现在同
一行, 只要它们之间用分号分开即可. 一个分号可以放在任何语句的末尾.

动作的左花括号必须与它的模式在同一行; 而剩下的部分, 包括右花括号, 则可以出
现在下面几行.

空行会被忽略; 它们可以插入在语句之前或之后, 用于提高程序的可读性. 空格与制
表符可以出现在运算符与操作数的周围, 同样也是为了提高可读性.

注释可以出现在任意一行的末尾. 一个注释以井号 (\verb'#') 开始,
以换行符结束, 正如
\begin{awkcode}
    { print $1, $3 }    # print country name and population
\end{awkcode}

一条长语句可以分散成多行, 只要在断行处插入一个反斜杠即可:
\begin{awkcode}
    { print \
            $1,     # country name
            $2,     # area in thousands of square miles
            $3 }    # population in millions
\end{awkcode}
正如这个例子所呈现的那样, 语句可以在逗号之后断行, 并且注释可以出现在断行的
末尾.

在这本书里我们用到了若干种编程风格, 之所以这样做, 一方面是为了比较不同
风格之间的差异, 另一方面是为了避免程序占用过多的行. 对于比较短小的程序
--- 就像本章中出现过的那些例子 ---
格式并不是非常重要, 但是一致性与可读性对于大程序
的管理非常有帮助.

\section{模式}
\label{sec:patterns}
\marginpar{23}

模式控制着动作的执行: 当模式匹配时, 相应的动作便会执行. 
这一小节描述模式的6种类型, 以及匹配它们的条件.
\begin{summary}{模式汇总}
    \begin{enumerate}
        \item \BEGIN \verb'{' \stmt \verb'}' \par
            在输入被读取之前, \stmt 执行一次.
        \item \END \verb'{' \stmt \verb'}'      \par
            当所有输入被读取完毕之后, \stmt 执行一次.
        \item \expr \verb'{' \stmt \verb'}' \par
            每碰到一个使 \expr 为真的输入行, \stmt 就执行. \expr 为真指
            的是其值非零或非空.
        \item \verb'/'\regexpr\verb'/' \verb'{' \stmt \verb'}' \par
            当碰到这样一个输入行时, \stmt 就执行: 输入行含有一段字符串,
            而该字符串可以被 \regexpr 匹配.
        \item \textit{compound pattern} \verb'{' \stmt \verb'}' \par
            一个复合模式将表达式用 \AND{}(AND), \OR{}(OR), \NOT{}(NOT), 以及
            括号组合起来; 当 \textit{compound pattern} 为真时, \stmt 执行.
        \item \pat$_1$\verb',' \pat$_2$ \verb'{' \stmt{}\verb'}' \par
            一个范围模式匹配多个输入行, 这些输入行从匹配 \pat$_1$ 的行开始,
            到匹配 \pat$_2$ 的行结束 (包括这两行), 对这其中的每一行执行
            \stmt.\footnote{\pat$_1$ 与 \pat$_2$ 可以匹配同一行. --- 译者注}
    \end{enumerate}
    \BEGIN 与 \END 不与其他模式组合. 一个范围模式不能是其他模式的一部分.
    \BEGIN 与 \END 是唯一两个不能省略动作的模式.
\end{summary}

\subsection{\BEGIN 与 \END}
\label{subsec:the_awk_language_begin_and_end}

\BEGIN 与 \END 这两个模式不匹配任何输入行. 实际情况是, 当 awk 从输入读取数据
之前, \BEGIN 的语句开始执行;
当所有输入数据被读取完毕, \END 的语句开始执行.
于是, \BEGIN 与 \END 分别提供了一种控制初始化与扫尾的方式. \BEGIN 与 \END
不能与其他模式作组合. 如果有多个 \BEGIN, 与其关联的动作会按照它们在程序中
出现的顺序执行, 这种行为对多个 \END 同样适用. 我们通常将 \BEGIN 放在程序
开头, 将 \END 放在程序末尾, 虽然这并不是强制的.

\BEGIN 的一个常见用途是更改输入行被分割为字段的默认方式. 分割字符由一个内
建变量 \fs 控制. 默认情况下字段由空格或 (和) 制表符分割, 此时 \fs 的值被%
\marginpar{24}%
设置为一个空格符. 将 \fs 设置成一个非空格字符, 就会使该字符成为字段分
割符.

下面这个程序在 \BEGIN 的动作里将字段字割符设置为制表符 (\verb'\t'), 并在
输出之前打印标题. 第二个 \printf 语句 (对每一个输入行它都会执行) 将输出
格式化成一张表格, 使得每一列都刚好与标题的列表头对齐. \END 打印总和. (变量
与表达式在 \ref{sec:actions} 节讨论.)
\begin{awkcode}
    # print countries with column headers and totals

    BEGIN { FS = "\t"   # make tab the field separator
            printf("%10s %6s %5s   %s\n\n",
                  "COUNTRY", "AREA", "POP", "CONTINENT")
          }
          { printf("%10s %6d %5d   %s\n", $1, $2, $3, $4)
            area = area + $2
            pop = pop + $3
          }
    END   { printf("\n%10s %6d %5d\n", "TOTAL", area, pop) }
\end{awkcode}
如果将 \filename{countries} 作为输入, 将出将是
\begin{awkcode}
   COUNTRY   AREA   POP   CONTINENT

      USSR   8649   275   Asia
    Canada   3852    25   North America
     China   3705  1032   Asia
       USA   3615   237   North America
    Brazil   3286   134   South America
     India   1267   746   Asia
    Mexico    762    78   North America
    France    211    55   Europe
     Japan    144   120   Asia
   Germany     96    61   Europe
   England     94    56   Europe

     TOTAL  25681  2819
\end{awkcode}

\subsection{将表达式用作模式}
\label{subsec:expressions_as_patterns}

就像大多数程序设计语言一样, awk 拥有非常丰富的用来描述数值计算的表达式,
但是与许多语言不同的是, awk 还有用于描述字符串操作的表达式. 贯穿全书,
\textit{string} 都表示一个由 0 个或多个字符组成的序列. 这些字符串可以存储
在变量中, 也可以以字符串常量的形式出现, 就像 \verb'""' 或 \verb'"Asia"'.
字%
符串 \verb'""' 不包括任何字符, 叫做 \cterm{空字符串} (\term{null string}).
术语 \cterm{子字符串} (\term{substring}) 表示一个字符串内部的, 由0个或多个
字符组成的连续序列. 对任意一个字符串, 空字符串都可以看作是该字符串第一个字
符之前的, 长度为0的子字符串, 或者是一对相邻字符之间的子字符串, 又或者是
最后一个字符之后的子字符串.

任意一个表达式都可以用作任意一个运算符的操作数.
如果一个表达式拥有一个数值形式%
\marginpar{25}%
的值, 而运算符要求一个字符串值, 那么该数值会自动转换成字符串, 类似地, 当
运算符要求一个数值时, 字符串被自动转换成数值.

任意一个表达式都可以当作模式来使用. 如果一个作为模式使用的表达式, 对当前
输入行的求值结果非零或不为空, 那么该模式就匹配该行. 典型的表达式模式是那些
涉及到数值或字符串比较的表达式. 一个比较表达式包含 6 种关系运算符中的一种,
或者包含两种字符串匹配运算符中的一种: \verb'~' 与 \verb'!~' 在下一小节讨论.
关系运算符列在表 \ref{tbl:comparison_operators} 中.
\begin{figure}[ht]
\captionsetup{type=table}
\caption{比较运算符}
\label{tbl:comparison_operators}
\begin{center}
\begin{tabular}{c|c}
    \hline
    \hline
    运算符      & 意义 \\
    \hline
    \verb'<'    & 小于 \\
    \verb'<='   & 小于或等于 \\
    \verb'=='   & 等于  \\
    \verb'!='   & 不等于 \\
    \verb'>='   & 大于或等于 \\
    \verb'>'    & 大于  \\
    \verb'~'    & 匹配 \\
    \verb'!~'   & 不匹配 \\
    \hline
\end{tabular}
\end{center}
\end{figure}

如果模式是一个比较表达式, 就像 \verb'NF > 10', 当当前行使该条件满足时, 这
个模式就算是匹配该输入行, 在这里条件满足指的是当前输入行的字段数大于 10.
如果模式是一个算术表达式, 就像 \nf, 如果该表达式的值非零, 那么当前输入行被
匹配. 如果模式是一个字符串表达式, 当表达式的字符串值非空时, 当前输入行被
匹配.

在一个关系比较中, 如果两个操作数都是数值, 关系比较将会按照数值比较进行;
否则的话, 数值操作数会被转换成字符串, 再将操作数按字符串的形式进行比较.
两个字符串间的比较以字符为单位逐个相比,
字符间的先后顺序依赖于机器的字符
集 (大多数情况下是 ASCII 字符集). 一个字符串 ``小于'' 另一个, 指的是它比另
一个字符串更早出现, 例如 \verb'"Canada" < "China"',
\verb'"Asia" < "Asian"'.

模式
\begin{awkcode}
    $3/$2 >= 0.5
\end{awkcode}
选择的行, 其第3个字段除以第2个字段所得的商大于 0.5, 而
\begin{awkcode}
    $0 >= "M"
\end{awkcode}
\marginpar{26}
选择那些以字母 \verb'M', \verb'N', \verb'O' 等开头的输入行:
\begin{awkcode}
    USSR        8649    275     Asia
    USA         3615    237     North America
    Mexico      762     78      North America
\end{awkcode}

有时候一个比较运算符的类型不能单单靠表达式表现出来的语法形式来判断. 程序
\begin{awkcode}
    $1 < $4
\end{awkcode}
可以以数值的形式, 或者字符串的形式比较输入行的第1个与第4个字段. 在这里,
比较的类型取决于字段的值, 并且有可能每一行都有不同的情况出现. 文件
\filename{countries} 的第1个与第4个字段总是字符串, 所以比较总是以字符串的形
式进行; 输出是
\begin{awkcode}
    Canada      3852    25      North America
    Brazil      3286    134     South America
    Mexico      762     78      North America
    England     94      56      Europe
\end{awkcode}
只有当两个字段都是数值时, 比较才会以数值的形式进行; 这种情况可以是
\begin{awkcode}
    $2 < $3
\end{awkcode}

\ref{sec:actions} 节包含了一个更加完整的, 关于字符串, 数值与表达式的讨论.

\subsection{字符串匹配 模式}
\label{subsec:string_matching_pattern}

Awk 提供了一种称为 \cterm{正则表达式} (\regexpr) 的表示法, 它可以用来指
定和
匹配一个字符串.  正则表达式 在 Unix 环境用得非常普遍, 包括文本编辑器与
shell. 受限形式的正则表达式也出现在其他系统中, 在 MS-DOS 中可以用
``通配符'' 指定一个文件名集合.

一个 \cterm{字符串匹配模式} (\term{string-matching pattern})
测试一个字符串是否包含一段可以被正则表达式匹配的子字符串.

最简单的正则表达式是仅由数字与字母组成的字符串, 就像 \verb'Asia', 它匹配的
就是它本身. 为了将一个正则表达式切换成一个模式, 只需要用一对斜杠包围起来即
可:
\begin{awkcode}
    /Asia/
\end{awkcode}
这个模式匹配那些含有子字符串 \verb'Asia' 的输入行, 例如 \verb'Asia',
\verb'Asian', 或 \verb'Pan-Asiatic'. 注意, 正则表达式中空格是有意义的: 字
符串匹配模式
\marginpar{27}
\begin{summary}{字符串匹配模式}
    \begin{enumerate}
        \item \verb'/'\textit{regexpr}\verb'/' \par
            如果当前输入行包含一段能够被 \textit{regexpr} 匹配的子字符串,
            则该模式被匹配.
        \item \expr {} \verb'~' \verb'/'\textit{regexpr}\verb'/' \par
            如果 \expr 的字符串值包含一段能够被 \textit{regexpr} 匹配的子
            字符串, 则该模式被匹配.
        \item \expr {} \verb'!~' \verb'/'\textit{regexpr}\verb'/' \par
            如果 \expr 的字符串值不包含能够被 \textit{regexpr} 匹配的子字
            符串, 则该模式被匹配.
    \end{enumerate}
    在 \verb'~' 与 \verb'!~' 的语境中, 任意一个表达式都可以用来替换
    \verb'/'\textit{regexpr}\verb'/'.
\end{summary}

\begin{awkcode}
    / Asia /
\end{awkcode}
只有当 \verb'Asia' 被一对空格包围时才会匹配成功.

上面的模式是三种字符串匹配模式当中的一种. 它的形式是用一对斜杠将正则表达式
包围起来:
\begin{pattern}
    \verb'/'\textit{r}\verb'/'
\end{pattern}
如果某个输入行含有能被 \textit{r} 匹配的子字符串, 则该行匹配成功.

剩下的两种字符串匹配模式使用到了显式的匹配运算符:
\begin{pattern}
    \expr {} \verb'~' \verb'/'\textit{r}\verb'/' \par
    \expr {} \verb'!~' \verb'/'\textit{r}\verb'/'
\end{pattern}
匹配运算符 \verb'~' 的意思是 ``被...匹配'', \verb'!~' 的意思是 ``不被...%
匹配''. 当 \expr 的字符串值包含一段能够被正则表达式 \textit{r} 匹配的子字
串时, 第一个模式被匹配; 当不存在这样的子字符串时, 第二个模式被匹配.

匹配运算符的左操作数经常是一个字段, 模式
\begin{awkcode}
    $4 ~ /Asia/
\end{awkcode}
匹配所有{}第4个字段包含 \verb'Asia' 的输入行, 而
\begin{awkcode}
    $4 !~ /Asia/
\end{awkcode}
匹配所有{}第4个字段\cterm{不}包含 \verb'Asia' 的输入行.

注意到, 字符串匹配模式
\begin{awkcode}
    /Asia/
\end{awkcode}
是
\begin{awkcode}
    $0 ~ /Asia/
\end{awkcode}
的简写形式.

\subsection{正则表达式}
\label{subsec:regular_expressions}
\marginpar{28}
正则表达式是一种用于指定和匹配字符串的表示法. 就像算术表达式一样,
一个正则表达式是一个基本表达式, 或者是多个子表达式通过运算符组合而成.
为了理解被一个正则表达式匹配的字符串, 我们需要先了解被子表达式匹配的字符串.
\begin{summary}{正则表达式}
\begin{enumerate}
\item 正则表达式的元字符包括: \par
    \verb'\ ^ $ . [ ] | ( ) * + ?'
\item 一个基本的正则表达式包括下面几种: \par
    一个不是元字符的字符, 例如 \verb'A', 这个正则表达式匹配的就是它本身. \par
    一个匹配特殊符号的转义字符: \verb'\t' 匹配一个制表符 (见表
    \ref{tbl:escape_sequences}). \par
    一个被引用的元字符, 例如 \verb'\*', 按字面意义匹配元字符. \par
    \verb'^' 匹配一行的开始. \par
    \verb'$' 匹配一行的结束. \par
    \verb'.' 匹配任意一个字符. \par
    一个字符类: \verb'[ABC]' 匹配字符 \verb'A', \verb'B' 或 \verb'C'. \par
    字符类可能包含缩写形式: \verb'[A-Za-z]' 匹配单个字母. \par
    一个互补的字符类: \verb'[^0-9]' 匹配任意一个不是数字的字符.
\item 这些运算符将正则表达式组合起来:   \par
    选择: \verb'A|B' 匹配 \verb'A' 或 \verb'B'. \par
    拼接: \verb'AB' 匹配后面紧跟着 \verb'B' 的 \verb'A'. \par
    闭包: \verb'A*' 匹配0个或多个 \verb'A'. \par
    正闭包: \verb'A+' 匹配一个或多个 \verb'A'. \par
    零或一: \verb'A?' 匹配空字符串或 \verb'A'. \par
    括号: 被 \verb'('\textit{r}\verb')' 匹配的字符串, 与 \textit{r} 所匹配 
    的字符串相同.
\end{enumerate}
\end{summary}

基本的正则表达式在上面的表格中列出. 字符
\begin{awkcode}
    \ ^ $ . [ ] | ( ) * + ?
\end{awkcode}
叫作 \cterm{元字符} (\term{metacharacter}), 之所以这样称呼是因为它们具有
特殊的意义. 一个由单个非元字符构成的正则表达式匹配它自身. 于是, 一个字母
或一个数字都算作是一个基本的正则表达式, 与自身相匹配.  为了在正则表达式中
保留元字符的字面意义, 需要在字符的前面加上反斜杠. 于是, \verb'\$' 匹配
普通字符 \verb'$'. 如果某个字符前面冠有 \verb'\', 我们就说该字符是被
\cterm{引用} (\term{quoted}) 的.

在一个正则表达式中, 一个未被引用的脱字符 \verb'^' 表示一行的开始, 一个
未被引用的美元符 \verb'$' 匹配一行的结束, 一个未被引用的句点 \verb'.'
匹配任意一个字符. 于是,
\begin{tabbing}
\indent\verb'^C' \hspace{4em}  \= 匹配以字符 \verb'C' 开始的字符串; \\
\indent\verb'C$'   \> 匹配以字符 \verb'C' 结束的字符串; \\
\indent\verb'^C$'  \> 匹配只含有单个字符 \verb'C' 的字符串; \\
\indent\verb'^.$'  \> 匹配有且仅有一个字符的字符串; \\
\indent\verb'^...$' \> 匹配有且仅有3个字符的字符串; \\
\indent\verb'...'  \> 匹配任意3个字符; \\
\indent\verb'\.$'  \> 匹配以句点结束的字符串.
\end{tabbing}
\marginpar{29}

由一组被包围在方括号中的字符组成的正则表达式称为 \cterm{字符类}
(\term{character class}); 这个表达式匹配字符类中的任意一个字符. 例如,
\verb'[AEIOU]' 匹配 \verb'A', \verb'E', \verb'I', \verb'O' 或 \verb'U'.

使用连字符的字符类可以表示一段字符范围. 紧跟在连字符左边的字符定义了范围
的开始, 紧跟在连字符右边的字符定义了范围的结束. 于是, \verb'[0-9]' 匹配
任意一个数字, \verb'[a-zA-Z][0-9]' 匹配一个后面紧跟着一个数字的字母. 如果
左右两边都没有操作数, 那么字符类中的连字符就表示它本身, 所以 \verb'[+-]'
与 \verb'[-+]' 匹配一个 \verb'+' 或 \verb'-'. \verb'[A-Za-z-]+' 匹配一个
可能包含连字符的单词.

一个 \cterm{互补} (\term{complemented}) 的字符类在 \verb'[' 之后以
\verb'^' 开始. 这样一个类匹配任意一个不在类中的字符, ``类中的字符''%
指的
是方括号内排在脱字符之后的那些字符. 于是, \verb'[^0-9]' 匹配任意一个不是
数字的字符; \verb'[^a-zA-Z]' 匹配任意一个不是字母的字符.
\begin{tabbing}
    \indent\verb'^[ABC]' \hspace{4em}  \= 匹配以 \verb'A', \verb'B', 或 \verb'C'
        开始的字符串; \\
    \indent\verb'^[^ABC]' \> 匹配以任意一个字符 (除了 \verb'A', \verb'B',
        或 \verb'C') 开始的字符串; \\
    \indent\verb'[^ABC]' \> 匹配任意一个字符, 除了 \verb'A', \verb'B', 或
        \verb'C'; \\
    \indent\verb'^[^a-z]$' \> 匹配任意一个有且仅有一个字符的字符串, 且该字
        符不能是小写字母.
\end{tabbing}

在一个字符类中, 所有的字符都具有它自身的字面意义, 除了引用字符 \verb'\',
互补字符类开头的 \verb'^', 以及两个字符间的 \verb'-'.
于是, \verb'[.]' 匹配一个句点, \verb'^[^^]' 匹配不以脱字符开始的字符串.

可以使用括号来指定正则表达式中的各个成分如何组合. 有两种二元正则表达式运算
符: 选择与拼接. 选择运算符 \verb'|' 用来指定一个选择: 如果 \textit{r}$_1$
与 \textit{r}$_2$ 是正则表达式, 那么 \textit{r}$_1$\verb'|'\textit{r}$_2$
所匹配的字符串, 或者与 \textit{r}$_1$, 或者与 \textit{r}$_2$ 匹配.

Awk 不存在显式的拼接运算符. 如果 \textit{r}$_1$ 与 \textit{r}$_2$ 是正则
表达式, 那么 \verb'('\textit{r}$_1$\verb')'\verb'('\textit{r}$_2$\verb')'
(在 \verb'('\textit{r}$_1$\verb')' 与 \verb'('\textit{r}$_2$\verb')' 之
间没有空格) 所匹配的字符串具有形式 \textit{xy}, 其中 \textit{x} 被
\textit{r}$_1$ 匹配, \textit{y} 被 \textit{r}$_2$ 匹配. 如果被括号包
围的正则表达式不包含选择 运算符, 那么 \textit{r}$_1$ 或 \textit{r}$_2$
两边的括号就可以省略. 正则表达式
\begin{awkcode}
    (Asian|European|North American) (male|female) (black|blue)bird
\end{awkcode}
一共匹配12种字符串, 从
\begin{awkcode}
    Asian male blackbird
\end{awkcode}
到
\marginpar{30}
\begin{awkcode}
    North American female bluebird
\end{awkcode}

符号 \verb'*', \verb'+' 与 \verb'?' 是一元运算符, 用来指定正则表达式的
重复次数. 如果 \textit{r} 是一个正则表达式, 那么
\verb'('\textit{r}\verb')'\verb'*' 所匹配的字符串含有零个或连续多个
能被 \textit{r} 匹配的子符串. \textit{r}\verb'?' 匹配的字符串, 要么是空字
符串, 要么是能够被 \textit{r} 匹配的字符串. 如果 \textit{r} 是一个基本的
正则表达式, 那么括号可以省略.
\begin{tabbing}
\indent\verb'B*' \hspace{4em} \= 匹配空字符串, 或 \verb'B', \verb'BB', 等等.\\
\indent\verb'AB*C' \> 匹配 \verb'AC', 或 \verb'ABC', \verb'ABBC', 等等. \\
\indent\verb'AB+C' \> 匹配 \verb'ABC', 或 \verb'ABBC', \verb'ABBBC', 等等.\\
\indent\verb'AB?C' \> 匹配 \verb'AC' 或 \verb'ABC' \\
\indent\verb'[A-Z]+' \> 匹配由一个或多个大写字母组成的字符串. \\
\indent\verb'(AB)+C' \> 匹配 \verb'ABC', \verb'ABABC', \verb'ABABABC', 等等.
\end{tabbing}

在正则表达式中, 选择运算符 \verb'|' 的优先级最低, 然后是拼接运算, 最后是重
复运算符 \verb'*', \verb'+', 与 \verb'?'. 与算术表达式的规则一样, 优先
级高的运算符优先处理. 这种规则经常使得括号被省略: \verb'ab|cd' 等价于
\verb'(ab)|(cd)', \verb'^ab|cd*e$' 等价于 \verb'(^ab)|(c(d*)e$)'.

为了结束关于正则表达式的讨论, 这里列出了一些比较有用的字符串匹配模式的
例子, 这些例子都带有使用了一元与二元运算符的正则表达式, 同时还描述了能够
被该模式匹配的输入行. 回想一下, 如果当前输入行含有至少一个能够被
\textit{r} 匹配的子字符串, 那么模式 \verb'/'\textit{r}\verb'/'
匹配成功.
\begin{enumerate}
\item[] \verb'/^[0-9]+$/' \par
    \qquad 匹配含有且只含有数字的输入行.
\item[] \verb'/^[0-9][0-9][0-9]$/' \par
    \qquad 输入行有且仅有3个数字.
\item[] \verb'/^(\+|-)?[0-9]+\.?[0-9]*$/' \par
    \qquad 十进制小数, 符号与小数部分是可选的.
\item[] \verb'/^[+-]?[0-9]+[.]?[0-9]*$/' \par
    \qquad 也是匹配十进制小数, 带有可选的符号与小数部分.
\item[] \verb'/^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)([eE][+-]?[0-9]+)?$/' \par
    \qquad 浮点数, 符号与指数部分是可选的.
\item[] \verb'/^[A-Za-z][A-Za-z0-9]*$/' \par
    \qquad 一个字母, 后面再跟着任意多个字母或数字 (比如 awk 的变量名).
\item[] \verb'/^[A-Za-z]$|^[A-Za-z][0-9]$/' \par
    \qquad 一个字母, 又或者是一个后面跟着一个数字的字母 (比如 Basic 的变量名).
\item[] \verb'/^[A-Za-z][0-9]?$/' \par
    \qquad 同样是一个字母, 又或者是一个后面跟着一个数字的字母.
\end{enumerate}
在第3个例子中, 为了匹配元字符 \verb'+' 与 \verb'-' 的字面值, 必须在它们的
前面加上反斜杠, 而在字符类中则不需要, 所以第 3 项与 第 4 项的功能是等价
的.
\marginpar{31}

任意一个被一对斜杠包围的正则表达式都可以作为匹配运算符的右操作数, 程序
\begin{awkcode}
    $2 !~ /^[0-9]+$/
\end{awkcode}
打印那些第2个字段不全是数字的行.

在正则表达式与字符串内部, awk 使用一个特定的字符序列---{}\cterm{转义序列}
(\term{escape sequences}){}---去表示那些无法用其他方式表示的字符. 例如,
\verb'\n'
表示一个换行符, 它无法以其他方式出现在字符串或正则表达式中; \verb'\b' 表示
退格符; \verb'\t' 表示制表符; \verb'\007' 表示 ASCII 中的响铃符; \verb'\/'
表示一个斜杠. 转义序列在 awk 程序中才会有特殊的意义; 如果在数据中, 它们则
是普通的字符. 完整的转义序列名单在表 \ref{tbl:escape_sequences}.
\begin{figure}[ht]
\captionsetup{type=table}
\caption{转义序列}
\label{tbl:escape_sequences}
\begin{center}
\begin{tabular}{c|l}
    \hline
    \hline
    序列        &  \multicolumn{1}{c}{意义}  \\
    \hline
    \verb'\b'   & 退格  \\
    \verb'\f'   & 换页  \\
    \verb'\n'   & 换行 \\
    \verb'\r'   & 回车 \\
    \verb'\t'    & 制表符    \\
    \verb'\'\textit{ddd} & 八进制数 \textit{ddd}, \textit{ddd}
        含有 1 到 3 个数字, 每个数字的值在 0 到 7 之间 \\
    \verb'\'\textit{c} & 其他的字面意义上的 \textit{c}
        (举例来说, \verb'\\' 表示反斜杠, \verb'\"' 表示双引号) \\
    \hline
\end{tabular}
\end{center}
\end{figure}

表 \ref{tbl:regular_expressions} 总结了正则表达式, 以及它们所匹配的字符串.
运算符按优先级递增的顺序列出.
\begin{figure}[ht]
\captionsetup{type=table}
\caption{正则表达式}
\label{tbl:regular_expressions}
\begin{center}
\begin{tabular}{c|p{25em}}
    \hline
    \hline
    表达式  & \multicolumn{1}{c}{匹配}  \\
    \hline
    \textit{c}  & 非元字符 \textit{c}   \\
    \verb'\'\textit{c}   & 转义序列或字面意义上的 \textit{c} \\
    \verb'^'    & 字符串的开始 \\
    \verb'$'    & 字符串的结束  \\
    \verb'.'    & 任意一个字符 \\
    \verb'['\textit{c}$_1$\textit{c}$_2$...\verb']' & 任意一个在
        \textit{c}$_1$\textit{c}$_2$... 中的字符. \\
    \verb'[^'\textit{c}$_1$\textit{c}$_2$...\verb']'    & 任意一个不在
        \textit{c}$_1$\textit{c}$_2$... 中的字符. \\
    \verb'['\textit{c}$_1$\verb'-'\textit{c}$_2$...\verb']' & 任意一个在
        范围内的字符, 范围由 \textit{c}$_1$ 开始, 由 \textit{c}$_2$ 结束.
        \\
\verb'[^'\textit{c}$_1$\verb'-'\textit{c}$_2$...\verb']' & 任意一个不在
        范围内的字符, 范围由 \textit{c}$_1$ 开始,
        由 \textit{c}$_2$ 结束.  \\
    \textit{r}$_1$\verb'|'\textit{r}$_2$    & 任意一个被 \textit{r}$_1$
        或 \textit{r}$_2$ 匹配的字符串. \\
     \verb'('\textit{r}$_1$\verb')('\textit{r}$_2$\verb')'    &
         任意一个字串 \textit{xy}, 其中 \textit{r}$_1$ 匹配 \textit{x},
         而 \textit{r}$_2$ 匹配 \textit{y};
         如果当中不含有选择运算符, 那么
         括号是可以省略的 \\
     \verb'('\textit{r}\verb')*' & 零个或连续多个能被 \textit{r} 匹配
         的字符串. \\
     \verb'('\textit{r}\verb')+' & 一个或连续多个能被 \textit{r} 匹配
         的字符串. \\
     \verb'('\textit{r}\verb')?' & 零个或一个能被 \textit{r} 匹配
         的字符串. 在这里括号可以省略. \\
     \verb'('\textit{r}\verb')' & 任意一个能被 \textit{r} 匹配
         的字符串. \\
     \hline
\end{tabular}
\end{center}
\end{figure}

\subsection{复合模式}
\label{subsec:compound_patterns}

一个复合模式是一个组合了其他模式的表达式, 通过括号, 逻辑运算符 \OR{}(OR),
\AND{}(AND), \NOT{}(NOT) 进行组合. 如果表达式的值为真, 那么复合模式就匹配
当前输入行. 下面这个程序使用 AND 运算符选择那些第 4 个字段是 \verb'Asia'
且第 3 个字段大于 500 的行:
\marginpar{32}
\begin{awkcode}
    $4 == "Asia" && $3 > 500
\end{awkcode}
程序
\begin{awkcode}
    $4 == "Asia" || $4 == "Europe"
\end{awkcode}
使用 OR 运算符选择那些第 4 个字段是 \verb'Asia' 或 \verb'Europe' 的行.
因为后面这个查询是一个针对字符串的测试, 所以该程序的另一种写法是用到了
选择运算符的正则表达式:
\begin{awkcode}
    $4 ~ /^(Asia|Europe)$/
\end{awkcode}
(如果两个正则表达式匹配了相同的字符串集, 我们就说这两个正则表达式是
\cterm{等价} (\term{equivalent}) 的. 测试一下读者对正则表达式优先级
规则的理解程度, 下面这两个正则表达式等不等价: \verb'^Asia|Europe$' 与
\verb'^(Asia|Europe)$' ?)

如果在其他字段中没有出现 \verb'Asia' 或 \verb'Europe', 那么上面的模式也
可以写成
\begin{awkcode}
    /Asia/ || /Europe/
\end{awkcode}
或
\begin{awkcode}
    /Asia|Europe/
\end{awkcode}

运算符 \OR 优先级最低, 再往高是 \AND, 最高的是 \NOT. \AND 与 \OR 从左至
右计算操作数的值, 一旦已经知道整个表达式的值, 计算便停止.

\subsection{范围模式}
\label{subsec:range_patterns}

一个范围模式由两个被逗号分开的模式组成, 正如
\begin{pattern}
    \textit{pat}$_1$\verb',' \textit{pat}$_2$
\end{pattern}
一个范围模式匹配多个输入行, 这些输入行从匹配 \textit{pat}$_1$ 的行开始,
到匹配 \textit{pat}$_2$ 的行结束, 包括这两行; \textit{pat}$_2$ 可以与
\textit{pat}$_1$ 匹配到同一行, 这时候模式的范围大小就退化到了一行. 作为
一个例子, 模式
\begin{awkcode}
    /Canada/, /USA/
\end{awkcode}
匹配的行从包含 \texttt{Canada} 的行开始, 到包含 \texttt{USA} 的行结束.

一旦范围的第一个模式匹配到了某个输入行, 那么整个范围模式的匹配就开始了;
\marginpar{33}
如果范围模式的第二个模式一直都没有匹配到某个输入行, 那么范围模式会一直
匹配到输入结束:
\begin{awkcode}
    /Europe/, /Africa/
\end{awkcode}
输出
\begin{awkcode}
    France  211     55      Europe
    Japan   144     120     Asia
    Germany 96      61      Europe
    England 94      56      Europe
\end{awkcode}

在下一个例子里, 变量 \verb'FNR' 表示从当前输入文件中, 到目前为止读取到的
行数, 变量 \verb'FILENAME' 表示当前输入文件名; 它们两个都是内建变量. 于是,
程序
\begin{awkcode}
    FNR == 1, FNR == 5 { print FILENAME ": " $0 }
\end{awkcode}
打印每一个输入文件的前5行, 并在每一行的左边加上文件名.
这个程序也可以写成下面这种形式:
\begin{awkcode}
    FNR <= 5 { print FILENAME ": " $0 }
\end{awkcode}

一个范围模式不能是其他模式的一部分.

\subsection{模式总结}
\label{subsec:summary_of_patterns}

表 \ref{tbl:patterns} 总结了可以出现在 \patact 语句中的模式种类.
\begin{figure}[ht]
\begin{center}
\captionsetup{type=table}
\caption{模式}
\label{tbl:patterns}
\begin{tabular}{c|c|p{18em}}
    \hline
    \hline
    模式        & 例子      & \multicolumn{1}{c}{匹配}  \\
    \hline
    \BEGIN      & \BEGIN    & 输入被读取之前 \\
    \END        & \END      & 所有输入被读取完之后 \\
    \expr       & \verb'$3 < 100' & 第3个字段小于100的行 \\
    \textit{string-matching} & \verb'/Asia/' & 含有 \verb'Asia' 的行 \\
    \textit{compound}   & \verb'$3 < 100 && $4 == "Asia"' & 第3个字段小于
        100 并且第 4 个字段是  \verb'Asia' 的行 \\
    \textit{range} & \verb'NR==10, NR==20' & 输入的第10行到第20行. \\
    \hline
\end{tabular}
\end{center}
\end{figure}

\section{动作}
\label{sec:actions}
\marginpar{34}

在一个 \patact 语句中, 模式决定动作什么时候执行. 有时候动作会非常简单: 一条
单独的打印语句或赋值语句. 在有些时候, 动作有可能是多条语句, 语句之间用换
行符或分号分开. 这一小节通过讨论表达式与流程控制语句来开始对动作的描述. 
这节结束后, 将会讨论用户自定义函数与输入/输出语句.
\begin{summary}{动作}
    动作中的语句可以包括:
    \begin{itemize}
\item[] \indent      \expr, 包括常量, 变量, 赋值, 函数调用等等.
\item[] \indent      \print\ \textit{expression-list}
\item[] \indent      \printf\verb'('\fmt\verb', '\textit{expression-list}\verb')'
\item[] \indent      \verb'if ('\expr\verb')' \stmt
\item[] \indent      \verb'if ('\expr\verb')' \stmt\ \verb'else '\stmt
\item[] \indent      \verb'while ('\expr\verb') '\stmt
\item[] \indent      \verb'for ('\expr\verb'; '\expr\verb'; '\expr\verb') '\stmt
\item[] \indent      \verb'for ('\expr\verb' in '\textit{array}\verb')' \stmt
\item[] \indent      \verb'do '\stmt\ \verb'while ('\expr\verb')'
\item[] \indent      \texttt{break}
\item[] \indent      \ctn
    \end{itemize}
\end{summary}

\subsection{表达式}
\label{subsec:expressions}

我们从表达式开始讨论, 因为表达式是最简单的语句, 大多数其他语句都是由不同类
型的表达式组合而成. 初等表达式与其他表达式通过运算符组合在一起, 形成一个新的
表达式. 初等表达式是最原始的构造块: 它们包括常量, 变量, 数组引用, 函数调用,
以及各种内建变量, 例如字段的名字.

我们从常量与变量开始对表达式的讨论, 然后是运算符, 它们可以用来组合表达式.
这些表达式可以分成 5 种类别: 算术, 比较, 逻辑, 条件, 与赋值. 运算符之后,
讨论的是内建算术运算函数与字符串函数, 最后是数组.

\textbf{常量} (\emph{Constants}). Awk 中只有两种类型的常量: 字符串与数值.
将一个
字符序列用一对双引号包围起来就创建了一个字符串常量, 正如 \verb'"Asia"',
\marginpar{35}
或 \verb'"hello, world"' 或 \verb'""'. 字符串常量可以包含表
\ref{tbl:escape_sequences} 列出的转义序列.

一个数值常量可以是一个整数, 就像 \verb'1127', 或十进制小数, \verb'3.14',
或者是用科学计数法表示的数: \verb'0.707E-1'. 同一个数的不同表示法都拥有
相同的值: \verb'1e6', \verb'1.00E6', \verb'10e5', \verb'0.1e7' 与
\verb'1000000' 都表示同一个数. 所有的数都用浮点格式存储, 浮点数的精度依赖
于机器.

\textbf{变量} (\emph{Variables}). 表达式可以包含若干种类型的变量: 用户定义的,
内建的, 或字段. 用户定义的变量名字由数字, 字母与下划线构成, 但是名字不能
以数字开始. 所有内建变量的名字都只由大写字母组成.

每一个变量都有一个值, 这个值可以是字符串或数值, 或两者都是. 因为变量的类型
不需要事先声明, 所以 awk 需要根据上下文环境推断出变量的类型.
当需要时, awk 可
以把字符串转化为数值, 或反之. 例如, 在程序
\begin{awkcode}
    $4 == "Asia" { print $1, 100 * $2 }
\end{awkcode}
里, \verb'$2' 会被转换成数值, 如果它原来不是数值的话, 同样的道理, 如果 \verb'$1'
与 \verb'$4' 原来不是字符串, 它们会被转换成字符串.

一个未初始化的变量的值是 \verb'""' (空字符串) 与 0.

\textbf{内建变量} (\emph{Built-In Variables}). 表 \ref{tbl:built_in_variables}
列出了所有的内建变量. 其中一些我们已经见过了, 另一些会在本节或后面的章节里
用到. 这些变量可以用在所有的表达式中, 而且可以被用户重置. 每当有一个新的文件
被读取, \verb'FILENAME' 就会被重新赋值. 每当有一个新的记录被读进来,
\verb'FNR', \verb'NF', \verb'NR' 就会被重新赋值. 另外, 当 \verb'$0' 发生
改变, 或有新的字段被创建时, \verb'NF' 就被重置. \verb'RLENGTH' 与
\verb'RSTART' 会随着每一次 \verb'match' 的调用而改变.

\begin{figure}[ht]
\captionsetup{type=table}
\caption{内建变量}
\label{tbl:built_in_variables}
\begin{center}
\begin{tabular}{l|l|c}
	\hline
	\hline
	变量	& \multicolumn{1}{c|}{意义}	& 默认值 \\
	\hline
	\verb'ARGC'	& 命令行参数的个数	& - \\
	\verb'ARGV'	& 命令行参数数组	& - \\
	\verb'FILENAME'	& 当前输入文件名	& - \\
	\verb'FNR'	& 当前输入文件的记录个数& - \\
	\verb'FS'	& 控制着输入行的字段分割符 & \verb'" "' \\
	\verb'NF'	& 当前记录的字段个数	& - \\
	\verb'NR'	& 到目前为止读的记录数量 & - \\
	\verb'OFMT'	& 数值的输出格式	& \verb'"%.6g"' \\
	\verb'OFS'	& 输出字段分割符	& \verb'" "' \\
	\verb'ORS'	& 输出的记录的分割符	& \verb'"\n"'	\\
	\verb'RLENGTH'	& 被函数 \verb'match' 匹配的字符串的长度 & - \\
	\verb'RS'	& 控制着输入行的记录分割符 & \verb'"\n"'	\\
	\verb'RSTART'	& 被函数 \verb'match' 匹配的字符串的开始 \\
	\verb'SUBSEP'	& 下标分割符	& \verb'"\034"'	\\
	\hline
\end{tabular}
\end{center}
\end{figure}

\textbf{字段变量} (\emph{Field Variables}). 当前输入行的字段从 \verb'$1',
\verb'$2', 一直到 \verb'$NF'; \verb'$0' 表示整行. 字段变量与其他变量相比
没什么不同 --- 它们也可以用在算术或字符串运算中, 也可以被赋值. 于是, 人们
可以将 \filename{countries} 的每一行的第 2 个字段除以 1000, 从而可以用
百万平方英里 --- 而不是千平方英里 --- 来表示面积:
\begin{awkcode}
    { $2 = $2 / 1000; print }
\end{awkcode}
也可以将一个新的字符串赋给字段:
\begin{awkcode}
    BEGIN                   { FS = OFS = "\t" }
    $4 == "North America"	{ $4 = "NA" }
    $4 == "South America"	{ $4 = "SA" }
                            { print }
\end{awkcode}
在这个程序里, \BEGIN 动作重新设置 \verb'FS' (\verb'FS' 控制输入行的字段分
割符) 与 \verb'OFS' (\verb'OFS' 控制输出的字段分割符) 为 制表符. 第 4 行的
\print 语句打印可能被修改过的 \verb'$0'. 值得注意的是: 如果 \verb'$0' 发生了
\marginpar{36}
改变 (通过赋值或替换), 那么 \verb'$1', \verb'$2' 等等, 以及 \nf
就会被重新计算; 同样的道理, 当 \verb'$1' (或 \verb'$2' 等) 被修改了,
\verb'$0' 就会被重新构造, 构造的方式是使用 \OFS 重新分割字段.

字段也可以通过表达式指定. 例如, \verb'$(NF-1)' 表示当前输入行的倒数第 2 个
字段. 表达式两边的括号不能省略: \verb'$NF-1' 表示最后一个字段减1后的值.

如果字段变量引用到了不存在的字段, 例如 \verb'$(NF+1)', 那么它的值就是初始
值 --- 空字符串. 可以通过向一个字段变量赋值来创建它. 例如, 下面这个程序创建
了第 5 个字段, 该字段包含的值是人口密度:
\begin{awkcode}
    BEGIN { FS = OFS = "\t" }
    { $5 = 1000 * $3 / $2; print }
\end{awkcode}
当需要时, 任何突然出现的字段都会被创建, 并且它们的初始值都是空值.

每一行的字段数都可以不同, 但是 awk 的具体实现通常将字段数上限设置为 100.

\textbf{算术运算符} (\emph{Arithmetic Operators}). Awk 提供了通常的 \verb'+',
\verb'-', \verb'*', \verb'/', \verb'%', \verb'^' 运算符. 运算符 \verb'%'
计算余数: \verb'x%y' 的值是 \verb'x' 被 \verb'y' 除的余数; 当 \verb'x' 或
\verb'y' 是负数时, \verb'x%y' 的结果依赖于机器. \verb'^' 是指数运算符:
\verb'x^y' 表示 \verb'x'$^\texttt{y}$. 所有的算术运算都用的是浮点数.

\textbf{比较运算符} (\emph{Comparison Operators}). 比较表达式指的是那些含有
关系运算符, 或含有正则表达式匹配运算符的表达式. 关系运算符包括 \verb'<',
\verb'<=', \verb'==' (相等), \verb'!=' (不相等), \verb'>=' 与 \verb'>'.
\marginpar{37}
\begin{summary}{表达式}
    \begin{enumerate}
        \item 初等表达式包括: \par
            数值与字符串常量, 变量, 字段, 函数调用, 数组元素.
        \item 可以把表达式组合起来的运算符包括: \par
            赋值运算符 \verb'=  +=  -=  *=  /=  %=  ^='
        \par 条件表达式 \verb'?:'
        \par 逻辑运算符 \verb'||' (OR), \verb'&&' (AND), \verb'!' (NOT)
        \par 匹配运算符 \verb'~' 和 \verb'!~'
        \par 关系运算符 \verb'<  <=  ==  !=  >  >=' 
        \par 拼接运算符 (没有显式的拼接运算符)
        \par 算术运算符 \verb'+ - * / % ^'
        \par 单目运算符 \verb'+' 和 \verb'-'
        \par 自增与自减运算符 \verb'++' 和 \verb'--' (包括前缀与后缀)
        \par 括号 (用于分组)
    \end{enumerate}
\end{summary}
正则表达式的运算符包括 \verb'~' (被匹配) 与 \verb'!~' (不被匹配).
如果比较表达式的判断结果为真, 则它的值是 1, 否则为 0. 所以, 表达式
\begin{awkcode}
    $4 ~ /Asia/
\end{awkcode}
的值是 1, 如果当前输入行的第 4 个字段包含 \verb'Asia'的话; 反之, 如果不
包含, 那么它的值就是 0.

\textbf{逻辑运算符} (\emph{Logical Operators}). 逻辑运算符将多个表达式组合
成逻辑表达式. 如果逻辑表达式为真, 那么它的值就为 1; 如果为假, 值
就为 0. 在对逻辑表达式求值时, 具有非零值或非空值的操作数被当作真; 相应的,
值为零或空的操作数被当作假. 操作数之间被 \AND  或 \OR 分开, 求值是从左至
右进行的, 当整个逻辑表达式的值可以确定时, 求值就停下来. 这意味着在表达式
\begin{pattern}
    \textit{expr}$_1$\ \AND\ \textit{expr}$_2$
\end{pattern}
中, 如果 \textit{expr}$_1$ 的值为假, 那么 \textit{expr}$_2$ 就不会被求值,
而在表达式
\begin{pattern}
    \textit{expr}$_3$\ \OR\ \textit{expr}$_4$
\end{pattern}
中, 如果 \textit{expr}$_3$  的值为真, \textit{expr}$_4$ 就不会被求值.

在 \AND  与 \OR 之后可以插入换行符.

\textbf{条件表达式} (\emph{Conditional Expressions}). 一个条件表达式具有形式:
\begin{pattern}
    \textit{expr}$_1$ \verb'?' \textit{expr}$_2$ \verb':' \textit{expr}$_3$
\end{pattern}
首先, \textit{expr}$_1$ 被求值. 如果值为真, 也就是值非零或非空, 那么整个
条件表达式的值就会是 \textit{expr}$_2$ 的值; 否则, 如果 \textit{expr}$_1$
的值为假, 那么条件表达式的值就会是 \textit{expr}$_3$. \textit{expr}$_2$ 与
\marginpar{38}
\textit{expr}$_3$ 只有其中一个会被求值.

下面这个程序利用条件表达式打印 \verb'$1' 的倒数, 如果 \verb'$1' 的值为 0,
那就打印一条警告:
\begin{awkcode}
    { print ($1 != 0 ? 1/$1 : "$1 is zero, line " NR) }
\end{awkcode}

\textbf{赋值运算符} (\emph{Assignment Operators}). 在赋值表达式中可以使用 7
种赋值运算符. 最简单的赋值表达式是
\begin{pattern}
    \textit{var} \verb'=' \textit{expr}
\end{pattern}
在这个表达式中, \textit{var} 是一个变量或字段的名字, 然后 \textit{expr} 是
一个任意的表达式. 例如, 为了计算亚洲国家的总人口与数量, 我们可以写
\begin{awkcode}
    $4 == "Asia"    { pop = pop + $3; n = n + 1 }
    END             { print "Total population of the", n,
                            "Asian countries is", pop, "million."
                    }
\end{awkcode}
将 \filename{countries} 作为输入数据, 输出将是
\begin{awkcode}
    Total population of the 4 Asian countries is 2173 million.
\end{awkcode}
第一个动作含有两个赋值语句, 第一个累积人口, 第二个计算国家的数量. 变量没有
被显式地初始化, 但程序仍然按照期望得那样运行, 这是因为每个变量都会默认
初始化为空字符串 \verb'""' 或数值 0.

在下面这个程序里, 我们仍然利用了默认的初始化行为, 程序的功能是寻找
人口最多的国家:
\begin{awkcode}
    $3 > maxpop { maxpop = $3; country = $1 }
    END         { print "country with largest population:",
                    country, maxpop
                }
\end{awkcode}
注意, 只有当至少有一行的 \verb'$3' 是正数时, 程序才是正确的.

另外 6 个赋值运算符是 \verb'+=', \verb'-=', \verb'*=', \verb'/=', \verb'%=',
以及 \verb'^='. 它们的意义都是类似的: \textit{v op}\ \verb'='\ \textit{e}
等价
于 \textit{v}\ \verb'='\ \textit{v op e}, 但是 \textit{v} 只被求值一次.
赋值
表达式:
\begin{awkcode}
    pop = pop + $3
\end{awkcode}
可以用 \verb'+=' 写成更加紧凑的形式:
\begin{awkcode}
    pop += $3
\end{awkcode}
紧凑版与冗长版具有相同的作用 --- 左值加上右值, 并赋给左值 ---
但是 \verb'+=' 更简短, 运行起来也更快. 再看另外一个例子:
\begin{awkcode}
    { $2 /= 1000; print }
\end{awkcode}
这个程序将第 2 个字段除以 1000, 再打印整行.
\marginpar{39}

一个赋值语句是一个表达式; 整个表达式的值是左边的变量的新值. 于是, 赋值语句
可以放在任意表达式内部. 在复合赋值语句
\begin{awkcode}
    FS = OFS = "\t"
\end{awkcode}
中, 字段分割符与输出字段分割符都被设置为制表符. 赋值表达式在条件判断中也
很常见, 例如:
\begin{awkcode}
    if ((n = length($0)) > 0) ...
\end{awkcode}

\textbf{自增与自减} (\emph{Increment and Decrement Operators}). 赋值语句:
\begin{awkcode}
    n = n + 1
\end{awkcode}
通常写成 \verb'++n' 或 \verb'n++', 这里用到了一元自增运算符, 作用是给变量
加 1. 前缀形式 \verb'++n' 在传递 \verb'n' 的值之前为 \verb'n' 加 1; 后缀
形式 \verb'n++' 在传递 \verb'n' 的值之后, 为 \verb'n' 加 1. 当 \verb'++'
应用到赋值表达式时, 这会造成一些不一样的地方. 如果 \verb'n' 的初始值为 1,
赋值语句 \verb'i = ++n' 为 \verb'n' 加 1, 并将新值 \verb'2' 赋给 \verb'i',
而 \verb'i = n++' 同样为 \verb'n' 加 1, 但将旧值 \verb'1' 赋给 \verb'i'.
如果仅仅是给 \verb'n' 加 1, 那么 \verb'n++' 与 \verb'++n' 没什么区别.
前缀与后缀自减运算符 \verb'--' 给变量减 1, 除此之外与 \verb'++' 相同.

\textbf{内建算术函数} (\emph{Built-In Arithmetic Functions}). 内建算术函数在
表 \ref{tbl:built_in_arithmetic_functions} 中列出. 这些函数都可以作为初等
表达式使用. 在表格中, \textit{x} 与 \textit{y} 都是任意的表达式.

\begin{figure}[ht]
\captionsetup{type=table}
\caption{内建算术函数}
\label{tbl:built_in_arithmetic_functions}
\begin{center}
    \begin{tabular}{l|l}
        \hline
        \hline
        \multicolumn{1}{c|}{函数}    & \multicolumn{1}{c}{返回值}    \\
        \hline
        \verb'atan2('\textit{y}\verb','\textit{x}\verb')'   & $y/x$ 的反正切值,
        定义域在 $-\pi$ 到 $\pi$ 之间   \\
        \verb'cos('\textit{x}\verb')'   & \textit{x} 的余弦值, \textit{x}
        以弧度为单位    \\
        \verb'exp('\textit{x}\verb')'   & \textit{x} 的指数函数, $e^x$ \\
        \verb'int('\textit{x}\verb')'   & \textit{x} 的整数部分; 当
        \textit{x} 大于 0 时, 向 0 取整 \\
        \verb'log('\textit{x}\verb')'   & \textit{x} 的自然对数 (以 $e$ 为
        底)  \\
        \verb'rand()'   & 返回一个随机数 \textit{r}, 0 $\le$ \textit{r}
        < 1 \\
        \verb'sin('\textit{x}\verb')'   & \textit{x} 的正弦值, \textit{x}
        以弧度为单位.   \\
        \verb'sqrt('\textit{x}\verb')'  & \textit{x} 的方根 \\
        \verb'srand('\textit{x}\verb')' & \textit{x} 是 \verb'rand()' 的
        新的随机数种子  \\
        \hline
    \end{tabular}
\end{center}
\end{figure}

可以用这些函数来得到一些有用的常量: \verb'atan2(0,-1)' 得到 $\pi$,
\verb'exp(1)' 返回 $e$ --- 自然对数的底. 为了计算 \textit{x} 的以 10 为底
的对数, 我们可以用 \verb'log('\textit{x}\verb')/log(10)' 来实现.

\verb'rand()' 返回一个大于等于 0, 小于 1 的伪随机浮点数. 调用
\marginpar{40}
\verb'srand('\textit{x}\verb')' 可以使随机数生成器的开始点从 \textit{x} 开
始. 如果没有调用 \verb'srand', 每次程序运行时, \verb'rand'
都从同一个值开始.

赋值语句
\begin{awkcode}
    randint = int(n * rand()) + 1
\end{awkcode}
将 \verb'randint' 的值设置为 \verb'1' 到 \verb'n' 之间的一个整数, 包括
\verb'1' 与 \verb'n'. 这里我们用到函数 \verb'int' 来丢弃返回值的小数部分.
赋值语句
\begin{awkcode}
    x = int(x + 0.5)
\end{awkcode}
将正数 \verb'x' 四舍五入为最接近它的整数.

\textbf{字符串运算符} (\emph{String Operators}). Awk 中只有一种字符运算符 ---
拼接. 拼接并没有显式的运算符, 通过陆续写出字符串常量, 变量, 数组元素,
函数返回值, 与其他表达式, 就可以创建一个字符串. 程序
\begin{awkcode}
    { print NR ":" $0 }
\end{awkcode}
会将每一个输入行打印出来, 并在前面冠上行号, 以及一个冒号. 行号, 冒号与输入
行之间没有空格. 数值 \nr 会被自动转换成字符串 (如果必要的话, \verb'$0' 也
会做相同的转换); 然后三个字符串拼接在一起并打印出来.

\textbf{用作正则表达式的字符串} (\emph{Strings as Regular Expressions}).
到目前为止, 在所有的匹配表达式中, \verb'~' 与 \verb'!~' 右边的操作数都是
都是一个被斜杠包围起来的正则表达式. 实际上, 任意一个表达式都可以用作匹配
运算符的右操作数. Awk 对这些表达式求值, 如果必要的话将这些值转换成字符串,
再将这些字符串解释成正则表达式. 例如, 程序
\begin{awkcode}
    BEGIN { digits = "^[0-9]+$" }
    $2 ~ digits
\end{awkcode}
会将那些第 2 个字段有且仅有数字的行打印出来.

因为字符串可以被拼接起来, 于是一个正则表达式可以由多个子部分构成.
下面这个程序将那些具有有效浮点数的行打印出来:
\begin{awkcode}
    BEGIN {
        sign = "[+-]?"
        decimal = "[0-9]+[.]?[0-9]*"
        fraction = "[.][0-9]+"
        exponent = "([eE]" sign "[0-9]+)?"
        number = "^" sign "(" decimal "|" fraction ")" exponent "$"
    }
    $0 ~ number
\end{awkcode}
在一个匹配表达中, 一个被双引号包围的字符串, 可以与一个被斜杠包围的正则
\marginpar{41}
达式互换着使用, 例如 \verb'"^[0-9]+$"' 与 \verb'/^[0-9]+$/'. 然而有一个
例外, 如果被双引号包围的字符串想要匹配一个正则表达式元字符的字面值,
那就要在反斜杠的前面再加一个反斜杠来保护它. 于是
\begin{awkcode}
    $0 ~ /(\+|-)[0-9]+/
\end{awkcode}
与
\begin{awkcode}
    $0 ~ "(\\+|-)[0-9]+"
\end{awkcode}
是等价的.

这种性质可能看起来非常晦涩难懂, 但是, 被双引号包围的字符串被 awk 解析时,
起保护作用的反斜杠就会被移除, 这样想的话应该就容易理解多了. 如果一个正则
表达式的元字符需要一个反斜杠来暂时移除它的特殊意义, 那么在字符串中,
这个反斜杠就需要一个额外的反斜杠来保住它自己. 如果一个匹配运算符的右
操作数是一个变量或字段, 正如
\begin{awkcode}
    x ~ $1
\end{awkcode}
那么第 1 个字段中的反斜杠就不需要一层额外的保护, 因为在数据中反斜杠并没有
特殊的意义.

测试用户对正则表达式交互性的理解是很容易的: 程序
\begin{awkcode}
    $1 ~ $2
\end{awkcode}
允许用户输入一个字符串与一个正则表达式; 如果字符串匹配正则表达式的话, 它
就会回显该行.

\textbf{内建字符串函数} (\emph{Built-In String Functions}). 表
\ref{tbl:built_in_string_functions} 列出了 awk 的内建字符串函数. 在这张表
中, \textit{r} 表示一个正则表达式 (或者是一个字符串, 或者是被一对斜杠包围
了的). \textit{s} 与 \textit{t} 是字符串表达式, \textit{n} 与 \textit{p}
是整数.
\begin{figure}[ht]
    \captionsetup{type=table}
    \caption{内建字符串函数}
    \label{tbl:built_in_string_functions}
    \begin{center}
        \begin{tabular}{l|p{25em}}
            \hline
            \hline
            \multicolumn{1}{c|}{函数}    & \multicolumn{1}{c}{描述}  \\
            \hline
            \verb'gsub('\textit{r}\verb','\textit{s}\verb')' &  将
            \verb'$0' 中所有出现的 \textit{r} 替换为 \textit{s}, 返回
            替换发生的次数. \\
            \verb'gsub('\textit{r}\verb','\textit{s}\verb','\textit{t}
            \verb')'    & 将字符串 \textit{t} 中所有出现的 \textit{r}
            替换为 \textit{s}, 返回替换发生的次数 \\
            \verb'index('\textit{s}\verb','\textit{t}\verb')' & 返回字符串
            \textit{t} 在 \textit{s}中第一次出现的位置, 如果\textit{t}
            没有出现的话, 返回 0.   \\
            \verb'length('\textit{s}\verb')'    & 返回 \textit{s} 包含的
            字符个数 \\
            \verb'match('\textit{s}\verb','\textit{r}\verb')'   &
            测试 \textit{s} 是否包含能被 \textit{r} 匹配的子串,
            返回子串的起始位置或
            0; 设置 \verb'RSTART' 与 \verb'RLENGTH' \\
            \verb'split('\textit{s}\verb','\textit{a}\verb')'   &
            用 \verb'FS' 将 \textit{s} 分割到数组 \textit{a} 中, 返回字段
            的个数  \\
            \verb'split('\textit{s}\verb','\textit{a}\verb','\textit{fs}%
            \verb')'    & 用 \textit{fs} 分割 \textit{s} 到数组 \textit{a}
            中, 返回字段的个数  \\
            \verb'sprintf('\textit{fmt}\verb','\textit{expr-list}\verb')'
            & 根据格式字符串 \textit{fmt} 返回格式化后的 \textit{expr-list}
            \\
            \verb'sub('\textit{r}\verb','\textit{s}\verb')' &
            将 \verb'$0' 的最左最长的, 能被 \textit{r} 匹配的子字符串替换为
            \textit{s}, 返回替换发生的次数. \\
            \verb'sub('\textit{r}\verb','\textit{s}\verb','\textit{t}%
            \verb')'    & 把 \textit{t} 的最左最长的, 能被 \textit{r} 匹配
            的子字符串替换为 \textit{s}, 返回替换发生的次数.    \\
            \verb'substr('\textit{s}\verb','\textit{p}\verb')'  &
            返回 \textit{s} 中从位置 \textit{p} 开始的后缀.\\
            \verb'substr('\textit{s}\verb','\textit{p}\verb','\textit{n}%
            \verb')'    & 返回 \textit{s} 中从位置 \textit{p} 开始的, 长
            度为 \textit{n} 的子字符串. \\
            \hline
        \end{tabular}
    \end{center}
\end{figure}

函数 \verb'index('\textit{s}\verb','\textit{t}\verb')' 返回 \textit{t} 在
\textit{s} 中第一次出现的位置; 如果 \textit{t} 没有在 \textit{s} 中出现,
那就返回 0. 字符串第一个字符的位置是 1:
\begin{awkcode}
    index("banana", "an")
\end{awkcode}
返回 2.

函数 \verb'match('\textit{s}\verb','\textit{r}\verb')' 返回 \textit{s} 中
最左最长的, 能被 \textit{r} 匹配的子串, 返回值是子串在 \textit{s} 中的开始
位置, 如果没有找到相匹配的子串, 那就返回 0. 这个函数还会将内建变量
\verb'RSTART' 置为子串的起始位置, 将 \verb'RLENGTH' 置为子串的长度.

函数 \verb'split('\textit{s}\verb','\textit{a}\verb','\textit{fs}\verb')'
根据分割符 \textit{fs} 将字符串 \textit{s} 分割成一个个子串, 并存到数组
\textit{a} 中. 这个函数会在数组之后讨论, 也就是在这一节的末尾.

\marginpar{42}
\verb'sprintf('\fmt\verb','\textit{expr}$_1$\verb','\textit{expr}$_2$\verb','\ldots%
\verb','\textit{expr}$_n$\verb')' 返回 (不打印) 一个字符串,
这个字符串包含格式%
化了的
\textit{expr}$_1$, \textit{expr}$_2$, \ldots, \textit{expr}$_n$,
格式化的依据与 \printf 的格式说
明符相同, \printf 的格式说明符就是 \fmt. 于是, 语句
\begin{awkcode}
    x = sprintf("%10s %6d", $1, $2)
\end{awkcode}
把一个字符串赋值给 \verb'x', 这个字符串通过%
把 \verb'$1' 格式化成 10 个字符宽度的字符串, 把 \verb'$2' 格式化成至少 6
个字符宽度的十进制整数来生成. \ref{sec:output} 节包含了一个完整的, 关于
格式转换字符的描述.

自从 Unix 文件编辑器 \verb'ed' 的替换命令出现之后, \verb'sub' 与
\verb'gsub' 就已经被标准化了.
\verb'sub('\textit{r}\verb','\textit{s}\verb','\textit{t}\verb')' 首先在
目标字符串 \textit{t} 中找到能被 \textit{r} 匹配的最左最长子串,
再将这个子串
替换为 \textit{s}. 在 \verb'ed' 中, ``最左最长'' 意味着先找到最左边的匹配,
然后尽可能的延长匹配的长度.

如果目标字符串是 \verb'banana', 那么 \verb'anan' 就是正则表达式
\verb'(an)+' 的最左最长匹配. 相反, \verb'(an)*' 的最左最长匹配是 \verb'b'
左边的空字符串.

\verb'sub' 返回替换发生的次数.
\verb'sub('\textit{r}\verb','\textit{s}\verb')' 等价于
\verb'sub('\textit{r}\verb','\textit{s}\verb','\verb'$0'\verb')'.

\verb'gsub' 是类似的, 但是它会连续地替换最左最长且不重叠的子串, 而不是只
替换子串的第一次出现; 函数返回值仍然是替换发生的次数
\marginpar{43}
(\verb'gsub' 的 \verb'g' 是 ``global'' 的缩写, 表示替换是全局的). 例如,
程序
\begin{awkcode}
    { gsub(/USA/, "United States"); print }
\end{awkcode}
将输入行中的每一个 ``USA'' 替换为 ``United States'' (在这样的例子里,
当 \verb'$0' 发生改变时, 字段与 \nf 也会跟着发生改变). 程序
\footnote{在我的系统中 (mawk-1.3.3), \texttt{sub} 与 \texttt{gsub}
的第 3 个参数必须是变量. --- 译者}
\begin{awkcode}
    gsub(/ana/, "anda", "banana")
\end{awkcode}
将 \verb'banana' 变为 \verb'bandana', 匹配是不重叠的.

对一个由 \verb'sub' 或 \verb'gsub' 执行的替换来说, 字符 \verb'&' 在
\textit{s} 中任意一次出现都会被替换为被 \textit{r} 匹配的子串, 于是
\begin{awkcode}
    gsub(/a/, "aba", "banana")
\end{awkcode}
将 \verb'banana' 变为 \verb'babanabanaba'; 同样的效果也可用
\begin{awkcode}
    gsub(/a/, "&b&", "banana")
\end{awkcode}
来完成. 在 \verb'&' 的左边加上一个反斜杠就可以关闭它在替换字符串中的特殊
意义.

\verb'substr('\textit{s}\verb','\textit{p}\verb')'
返回 \textit{s} 从位置 \textit{p} 开始的后缀. 如果使用了
\verb'substr('\textit{s}\verb','\textit{p}\verb','\textit{n}\verb')',
那么只会返回后缀的
前 \textit{n} 个字符; 如果后缀的长度小于 \textit{n}, 那么就会返回整个后缀.
例如, 我们可以将国家名缩写为名字的前三个字母, 程序
\begin{awkcode}
    { $1 = substr($1, 1, 3); print $0 }
\end{awkcode}
输出
\begin{awkcode}
    USS 8649 275 Asia
    Can 3852 25 North America
    Chi 3705 1032 Asia
    USA 3615 237 North America
    Bra 3286 134 South America
    Ind 1267 746 Asia
    Mex 762 78 North America
    Fra 211 55 Europe
    Jap 144 120 Asia
    Ger 96 61 Europe
    Eng 94 56 Europe
\end{awkcode}
修改 \verb'$1' 使得 awk 重新计算 \verb'$0', 于是字段之间的分隔符就变成了
空格 (\verb'OFS' 的默认值), 而不是制表符.

只要将字符串陆续写出来就可以将它们拼接在一起. 例如, 对于
\filename{countries}, 程序
\begin{awkcode}
        { s = s substr($1, 1, 3) " " }
    END { print s}
\end{awkcode}
打印
\marginpar{44}
\begin{awkcode}
    USS Can Chi USA Bra Ind Mex Fra Jap Ger Eng
\end{awkcode}
程序每一次只为 \verb's' 构造一小段. (如果你很在意 \verb's' 的末尾的空格,
在 \verb'END' 中使用
\begin{awkcode}
    print substr(s, 1, length(s)-1)
\end{awkcode}
替换掉 \verb'print s')

\textbf{数值或字符串} (\emph{Number or String}). 表达式的值可以自动从
数值转换为字符串, 或反之, 具体取决于该值将用于什么运算. 算术表达式, 例如
\begin{awkcode}
    pop + $3
\end{awkcode}
这个表达式要求 \verb'pop' 与 \verb'$3' 都必须是数值, 如果它们原来不是数值
的话, 就会被强制转换成数值. 类似的, 赋值语句
\begin{awkcode}
    pop += $3
\end{awkcode}
也要求 \verb'pop' 与 \verb'$3' 是数值. 字符串表达式, 就像
\begin{awkcode}
    $1 $2
\end{awkcode}
要求 \verb'$1' 与 \verb'$2' 必须是字符串才能进行拼接, 所以如果必要的话,
它们就会被强制转换成字符串.

同样的运算符有时既可以用在数值上, 也可以用在字符串, 对于这种情况有一些特殊
的规则. 在赋值语句 \textit{v}\ \verb'='\ \textit{e} 中, 赋值语句本身与变量
\textit{v} 都会对表达式 \textit{e} 的类型有所要求. 比较表达式, 就像
\begin{awkcode}
    x == y
\end{awkcode}
如果两个操作数都是数值, 那么比较就按照数值进行; 否则, 数值类型的操作数被
强制转换成字符串, 然后再按字符串的方式进行比较.

现在让我们来查看一下这条规则具体应用时会产生什么影响, 语句
\begin{awkcode}
    $1 == $2
\end{awkcode}
涉及到字段. 在这个语句里, 比较的类型取决于这两个字段是否包含数值或字符串,
而这只有到程序运行时才可以知道; 对于不同的行, 比较的类型可能是不一样的.
当 awk 在运行状态下创建一个字段时, 会自动将它的类型设置为字符串; 另外,
如果字段包含一个机器可识别的数, 它也会给这个字段设置一个数值类型.

例如, 比较 \verb'$1 == $2' 将会按照数值进行, 并且比较结果为真, 如果
\verb'$1' 与 \verb'$2' 的值是下面任意一种的话,
\begin{awkcode}
    1   1.0    +1   1e0    0.1e+1   10E-1    001
\end{awkcode}
\marginpar{45}
这些值都是数值 1 的不同表示方法. 然而, 这个表达式也可以是字符串比较, 于是,
该表达式对下面每对值都会比较失败:
\begin{awkcode}
    0           (null)
    0.0         (null)
    0           0a
    1e500       1.0e500
\end{awkcode}
在前三对值中, 第二个字段都不是一个有效的数值. 对 \verb'1e500' 和
\verb'1.0e500' 来说, 如果它们的值大到机器无法表示, 就会被当成字符串进行比较
\footnote{把 \texttt{1e500} 与 \texttt{1.0e500} 转换成数值时如果发生溢出,
不同版本的 awk 处理方式也不同, gawk-4.0.1 返回一个特殊值, 此时
\texttt{1e500} 与 \texttt{1.0e500} 的数值是相等的, 而 mawk-1.3.3 则报错退出.
--- 译者注}.

输出语句
\begin{awkcode}
    print $1
\end{awkcode}
打印第 1 个字段的字符串值; 于是, 输出与输入是一样的.

未初始化的变量默认初始值为数值 0 或空字符串 \verb'""'.
不存在的, 或显式为空的字段具有字符串值 \verb'""', 它们不是数值, 但是当强制
转换为数值时, 将会是 0. 我们将在本节的末尾看到, 数组的下标是字符串.

有两种惯用语法可以将表达式从一种类型转换成另一种类型:
\begin{tabbing}
    \indent\textit{number} \verb'""'  \hspace{2em} \= 将空字符串拼接到
    \textit{number} 可以将它强制转换成字符串; \\
    \indent\textit{string} \verb'+ 0'  \> 给字符串加上零可以把它强制转换成数值.
\end{tabbing}
于是, 为了强制让两个字段之间的比较按照字符串来进行, 我们可以把其中一个字段
强制转换成字符串:
\begin{awkcode}
    $1 "" == $2
\end{awkcode}
为了让比较强制按照数值类型来进行, 我们需要将\textbf{两个}字段都转换成数值:
\begin{awkcode}
    $1 + 0 == $2 + 0
\end{awkcode}
无论字段包含什么内容, 这个方式总能奏效.

字符串的数值类型的值, 等于字符串中最长的看起来像数值的前缀的值. 于是
\begin{awkcode}
    BEGIN { print "1E2"+0, "12E"+0, "E12"+0, "1X2Y3"+0 }
\end{awkcode}
输出
\begin{awkcode}
    100 12 0 1
\end{awkcode}

数值的字符串形式需要根据 \verb'OFMT' 转换后才会输出. 为了拼接, 比较, 与
创建数组下标而需要把数值转换成字符串时, \verb'OFMT' 也会影响转换的过程.
\verb'OFMT' 的默认值是 \verb'"%.6g"'. 于是
\begin{awkcode}
    BEGIN { print 1E2 "", 12E-2 "", E12 "", 1.23456789 "" }
\end{awkcode}
输出
\begin{awkcode}
    100 0.12  1.23457
\end{awkcode}
\marginpar{46}
向 \verb'OFMT' 赋予新值就可以修改它的默认值. 比如将 \verb'OFMT' 改为
\verb'"%.2f"', 那么在打印数值, 以及通过强制转换而得到的数值之间进行比较时, 
小数点后将会保留两位小数.
\begin{figure}[ht]
    \captionsetup{type=table}
    \caption{表达式运算符}
    \label{tbl:expression_operators}
    \begin{center}
        \begin{tabular}{l|l|l|l}
        \hline
        \hline
        \multicolumn{1}{c|}{操作}    & \multicolumn{1}{c|}{运算符}    &
        \multicolumn{1}{c|}{例子}  & \multicolumn{1}{c}{例子的含义}    \\
        \hline
        赋值    & \verb'= += -= *=' \verb'/= %= ^=' & \verb'x *= 2' &
        \verb'x = x * 2' \\
        条件表达式 & \verb'?:' & \verb'x ? y : z' & 若 \verb'x' 为真, 则
        \verb'y', 否则 \verb'z' \\
        逻辑 或 & \verb'||' & \verb'x || y' & 若 \verb'x' 或 \verb'y' 为
        真, 则为 1, 否则为 0 \\
        逻辑 与 & \verb'&&' & \verb'x && y' & 若 \verb'x' 与 \verb'y' 都为
        真, 则为 1, 否则为 0 \\
        数组成员 & \verb'in' & \verb' i in a' & 如果 \verb'a[i]' 存在,
        则为 1, 否则为 0 \\
        匹配 & \verb'~'\ \ \verb'!~' & \verb'$1 ~ /x/' & 如果第一个字段包含
        \verb'x', 则为 1, 否则为 0 \\
        关系运算 & \verb'< <= == !=' \verb'>= >' & \verb'x == y' &
        如果 \verb'x' 等于 \verb'y', 则为 1, 否则为 0 \\
        拼接 & & \verb'"a" "bc"' & \verb'"abc"'; 不存在显式的拼接运算符 \\
        减法, 加法 & \verb'+ -' & \verb'x + y' & \verb'x' 与 \verb'y'
        的和 \\
        乘法, 除法, 取模 & \verb'* / %' & \verb'x % y' & \verb'x' 除以
        \verb'y' 的余数 \\
        单目加, 单目减 & \verb'+ -' & \verb'-x' & \verb'x' 的相反数 \\
        逻辑 非 & \verb'!' & \verb'!$1' & 若 \verb'$1' 为空或为 0, 则为 1,
        否则为 0 \\
        指数运算 & \verb'^' & \verb'x ^ y' & $\texttt{x}^\texttt{y}$ \\
        自增, 自减 & \verb'++ --' & \verb'++x, x++' & 为 \verb'x' 加 1 \\
        字段 & \verb'$' & \verb'$i+1' & 1 加上第 \verb'i' 个字段的值 \\
        组合 & \verb'( )' & \verb'($i)++' & 给第 \verb'i' 个字段的值加 1 \\
        \hline
        \end{tabular}
    \end{center}
\end{figure}

\textbf{运算符总结} (\emph{Summary of Operators}). 可以出现在表达式中的运算
符全部列在了表 \ref{tbl:expression_operators} 中. 将这些运算符应用到常量,
变量, 字段, 数组元素, 函数, 和其他表达式中, 就可以构造出一个表达式.

运算符按照优先级的升序排列. 优先级高的运算符优先求值; 举例来说,
乘法运算在加法运算之前求值. 所有的运算符都是左结合的, 除了赋值运算符,
条件运算符, 指数运算 --- 它们都是右结合的. 左结合性意味着相同优先级的运算符
按照从左到右的顺序进行运算; 于是 \verb'3-2-1' 是指 \verb'(3-2)-1', 而不是
\verb'3-(2-1)'.

因为没有显式的拼接运算符, 所以比较明智的做法是, 在拼接运算中,
将涉及到其他运算的%
\marginpar{47}%
表达式用括号包围起来. 考虑下面这个程序
\begin{awkcode}
    $1 < 0 { print "abs($1) = " -$1 }
\end{awkcode}
跟在 \print 后面的表达式看起来像是拼接, 实际上是一个减法运算. 程序
\begin{awkcode}
    $1 < 0 { print "abs($1) = " (-$1) }
\end{awkcode}
与
\begin{awkcode}
    $1 < 0 { print "abs($1) =", -$1 }
\end{awkcode}
都可以得到我们期望中的效果.

\subsection{流程控制语句}
\label{subsec:control_flow_statements}

Awk 提供花括号用于语句组合, \verb'if-else' 用于决策, \verb'while',
\verb'for', \verb'do' 语句用于循环, 所有这些都来源于 C 语言.

一条单独的语句总是可以被替换为一个被花括号包围起来的语句列表, 列表中的语句
用换行符或分号分开, 换行符可以出现在任何左花括号之后, 也可以出现在任何右
花括号之前.

\verb'if-else' 具有形式:
\begin{pattern}
    \verb'if ('\expr\verb')' \par
    \indent\indent\stmt$_1$ \par
    \verb'else' \par
    \indent\indent\stmt$_2$ \par
\end{pattern}
\verb'else '\stmt$_2$ 是可选的. 右括号, \stmt$_1$, 和关键词 \verb'else' 后
面的换行符是可选的. 如果 \verb'else' 与 \stmt$_1$ 出现在同一行, 并且
\stmt$_1$ 是一条单独的语句, 那么在 \stmt$_1$ 的末尾必须使用分号来终止语句.

在一个 \verb'if-else' 语句中, 表达式 \expr 先被求值, 如果 \expr 为真 (也就是
非空或非零), 那么 \stmt$_1$ 就会执行. 如果 \expr 为假 (也就是空或零),
如果 \verb'else '\stmt$_2$ 存在, 那它就会执行.

为了避免歧义, 我们规定, \verb'else' 与最近一个未匹配的 \verb'if' 匹配.
举例来说, 语句
\begin{awkcode}
    if (e1) if (e2) s=1; else s=2
\end{awkcode}
中的 \verb'else' 与第 2 个 \verb'if' 匹配. (\verb's=1' 后面的分号是必须的,
因为 \verb'else' 与 \verb's=1' 出现在同一行)

当条件为真时, \verb'while' 就会重复执行一条语句:
\begin{summary}{流程控制语句}
    \begin{enumerate}
        \item \verb'{'\stmt\verb'}' \par
            语句组
        \item \verb'if ('\expr\verb')'\stmt \par
            如果 \expr 为真, 执行 \stmt
        \item \verb'if ('\expr\verb')' \stmt$_1$ \verb'else' \stmt$_2$ \par
            如果 \expr 为真, 执行 \stmt$_1$, 否则执行 \stmt$_2$
        \item \verb'while ('\expr\verb')' \stmt \par
            如果 \expr 为真, 执行 \stmt; 然后重复前面的过程
        \item \verb'for ('\expr$_1$\verb';'\expr$_2$\verb';'\expr$_3$
            \verb')' \stmt \par
            等价于 \expr$_1$\verb';' \verb'while ('\expr$_2$\verb') {'
            \stmt\verb';' \expr$_3$\verb'}'
        \item \verb'for ('\textit{variable} \verb'in'
            \textit{array}\verb')' \stmt \par
            轮流地将 \textit{variable} 设置为 \textit{array} 的每一个下标,
            并执行 \stmt
        \item \verb'do' \stmt\ \verb'while ('\expr\verb')' \par
            执行 \stmt; 如果 \expr 为真就重复
        \item \verb'break' \par
            马上离开最内层的, 包围 \verb'break' 的 \verb'while',
            \verb'for' 或 \verb'do'
        \item \verb'continue'
            开始最内层的, 包围 \verb'continue' 的 \verb'while', \verb'for',
            或 \verb'do' 的下一次循环
        \item \verb'next' \par
            开始输入主循环的下一次迭代
        \item \verb'exit'
        \item \verb'exit' \expr \par
            马上执行 \END 动作; 如果已经在 \END 动作内, 那就退出程序.
            将 \expr 作为程序的退出状态返回.
    \end{enumerate}
\end{summary}
\marginpar{48}
\begin{pattern}
    \verb'while ('\expr\verb')' \par
    \indent\indent\stmt
\end{pattern}
右括号后面的换行符是可选的. 在这个循环语句里, \expr 被求值; 如果它的值为
真, \stmt 就会执行, 然后 \expr 再被求值. 只要 \expr 为真, 循环就会一直
持续下去. 举例来说, 下面这个程序打印所有的输入字段, 每行一个:
\begin{awkcode}
    {   i = 1
        while (i <= NF) {
            print $i
            i++
        }
    }
\end{awkcode}
当 \verb'i' 到达 \verb'NF+1' 时, 循环停止, 这也是循环结束时 \verb'i' 的值.

\verb'for' 语句是 \verb'while' 的更加一般的形式:
\begin{pattern}
    \verb'for ('\expr$_1$\verb';'\expr$_2$\verb';'\expr$_3$\verb')' \par
    \indent\indent\stmt
\end{pattern}
\marginpar{49}
右括号后面的换行符是可选的. \verb'for' 等价于
\begin{pattern}
    \expr$_1$ \par
    \verb'while ('\expr$_2$\verb') {' \par
        \indent\indent\stmt  \par
        \indent\indent\expr$_3$ \par
    \verb'}'
\end{pattern}
所以
\begin{awkcode}
    { for (i = 1; i <= NF; i++)
        print $i
    }
\end{awkcode}
所做的工作, 与前面的用 \verb'while' 循环遍历所有字段的例子是一样的. 在
\verb'for' 语句中, 三个表达式都是可选的. 如果 \expr$_2$ 被省略了, 那么条
件就被当作是永真式, 于是 \verb'for(;;)' 将会是无限循环.

\verb'for' 语句的一个替代版本可用于遍历数组下标, 这在数组那一节讨论.

\verb'do' 语句具有形式
\begin{pattern}
    \verb'do' \par
    \indent\indent\stmt\par
    \verb'while ('\expr\verb')'
\end{pattern}
关键字 \verb'do' 与 \stmt 后面的换行符是可选的. 如果 \verb'while' 与 \stmt
出现在了同一行, 并且 \stmt 是一条单独的语句, 那么就必须使用分号来终止
\stmt. \verb'do' 循环执行 \stmt 一次, 然后只要 \expr 为真, 就重复执行
\stmt. \verb'do' 循环与 \verb'while' , \verb'for' 循环相比有很大的不同:
它的条件测试在循环体的底部, 而不是顶部, 所以循环体至少会执行一次.

有两种语句可以影响循环的运行: \verb'break' 和 \verb'continue'. \verb'break'
会导致控制流马上从包围着它的循环内退出, 循环可以是 \verb'while', \verb'for'
, 或 \verb'do'. \verb'continue' 导致下一次迭代开始; 它使得执行流马上进入
\verb'while' 与 \verb'do' 的测试表达式, 或 \verb'for' 的 \expr$_3$.

\verb'next' 与 \verb'exit' 控制用于读取输入行的外层循环. \verb'next' 使
awk 抓取下一个输入行, 然后从第一个 \patact 语句开始匹配模式. 在 \END 动作
里, \verb'exit' 会导致程序终止, 在其他动作里, \verb'exit' 会使得程序表现
得就好像所有输入都读完了; 不再有输入会被读取, 并执行 \END 动作 (如果有 \END
的话).

如果 \verb'exit' 语句包含一个表达式
\begin{pattern}
    \verb'exit' \expr
\end{pattern}
\marginpar{50}
它使得 awk 将 \expr 的值作为程序的退出状态返回, 除非返回值被随后的错误或
\verb'exit' 覆盖. 如果没有 \expr, 退出状态就会是 0. 在某些操作系统中, 包括
Unix, 调用 awk 的程序可能会检查  awk 的退出状态.

\subsection{空语句}
\label{subsec:empty_statement}

单独一个分号表示一个空语句. 在下面这个程序里, \verb'for' 的循环体是一个
空语句.
\begin{awkcode}
    BEGIN { FS = "\t" }
          { for (i = 1; i <= NF && $i != ""; i++)
              ;
            if (i <= NF)
                print
          }
\end{awkcode}
这个程序打印所有的, 包含空字段的行.

\subsection{数组}
\label{subsec:arrays}

Awk 提供了一维数组, 用于存放字符串与数值. 数组与数组元素都不需要事先声明,
也不需要说明数组中有多少个元素. 就像变量一样, 当被提及时, 数组元素就会
被创建, 数组元素的默认初始值为 0 或空字符串
\verb'""'.

作为一个简单的例子, 语句
\begin{awkcode}
    x[NR] = $0
\end{awkcode}
将当前输入行赋值给数组 \verb'x' 的元素 \verb'NR'. 实际上, 将整个输入行
读入到一个数组中, 然后再按照某种方便的顺序去处理它们, 这样做是很容易的,
虽然会有点慢. 举例来说, 下面这个程序是 \ref{sec:arrays} 节的逆序打印程序
的变形:
\begin{awkcode}
        { x[NR] = $0 }
    END { for (i = NR; i > 0; i--) print x[i] }
\end{awkcode}
第一个动作仅仅是将每个输入行存放到数组 \verb'x' 中, 使用行号作为下标;
真正的工作在 \END 语句中完成.

Awk 的数组与大多数其他语言最大的不同点是, 数组元素的下标是字符串. 这个特性
使得 awk 具有了一种能力, 这种能力类似于 SNOBOL4 表格的关联内存, 也由于这个
原因, awk 的数组称为 \textbf{关联数组} (\emph{associative arrays}).

下面这个程序将 \verb'Asia' 与 \verb'Europe' 的人口数量累加到数组 \verb'pop'
中. \END 动作打印这两个洲的总人口.
\begin{awkcode}
    /Asia/      { pop["Asia"] += $3 }
    /Europe/    { pop["Europe"] += $3 }
    END         { print "Asian population is",
                    pop["Asia"], "million."
                  print "European population is",
                    pop["Europe"], "million."
                }
\end{awkcode}
\marginpar{51}
\verb'countries' 作为输入, 输出是
\begin{awkcode}
    Asian population is 2173 million.
    European population is 172 million.
\end{awkcode}
应该注意的是, 数组下标是字符串常量 \verb'"Asia"' 与 \verb'"Europe"'. 如果
我们将 \verb'pop["Asia"]' 写成 \verb'pop[Asia]', 后者使用变量 \verb'Asia'
的值作为下标, 又因为变量是未初始化过的, 于是人口数量将会累加到
\verb'pop[""]' 中.

这个例子其实并不需要关联数组, 因为这里只有两个元素, 并且都显式给出了名字.
假设我们的任务是计算每个大洲的总人口, 对于这种聚合问题, 使用关联数组是
非常方便的做法. 数组下标可以是任意的表达式, 所以
\begin{awkcode}
    pop[$4] += $3
\end{awkcode}
使用当前输入行的第 4 个字段的字符串值来索引数组 \verb'pop', 并使用该项来
累积第 3 个字段的值:
\begin{awkcode}
    BEGIN   { FS = "\t" }
            { pop[$4] += $3 }
    END     { for (name in pop)
                print name, pop[name]
            }
\end{awkcode}
数组 \verb'pop' 的下标是大洲的名字; 每一个元素的值是大洲的总人口. 无论大
洲的数目有多少个, 这个程序都可以正常地工作; 文件 \filename{countries}
的输出是
\begin{awkcode}
    Europe 172
    Asia 2173
    South America 134
    North America 340
\end{awkcode}
上面的程序使用 \verb'for' 语句的另一种形式来遍历数组下标:
\begin{pattern}
    \verb'for ('\textit{variable} \verb'in' \textit{array}\verb')' \par
        \indent\indent\stmt
\end{pattern}
这个循环将 \textit{variable} 轮流地设置为数组的每一个下标, 并执行 \stmt.
下标的排列顺序依赖于实现. 如果 \stmt 往数组里加入了新元素, 那么结果是不可
预知的.

为了判断某个特定的下标是否出现在数组中, 可以这样写:
\begin{pattern}
    \textit{subscript} \verb'in ' \textit{A}
\end{pattern}
如果 \textit{A}\verb'['\textit{subscript}\verb']' 已经存在, 那么这个表达式
\marginpar{52}
的值为 1, 否则为 0. 所以, 为了测试 \verb'Africa' 是否是数组 \verb'pop' 的
一个下标, 可以这样写:
\begin{awkcode}
    if ("Africa" in pop) ...
\end{awkcode}
这个条件判断可以在不产生副作用的情况下执行测试, 副作用指的是创建
\verb'pop["Africa"]', 如果写成下面这种样子, 就会有创建新元素的副
作用:
\begin{awkcode}
    if (pop["Africa"] != "") ...
\end{awkcode}

\textbf{\texttt{delete} 语句} (\emph{The \texttt{delete} Statement}).
一个数组元素可以通过
\begin{pattern}
    \verb'delete' \textit{array}\verb'['\textit{subscript}\verb']'
\end{pattern}
删除. 例如, 下面这个循环删除数组 \verb'pop' 中所有的元素:
\begin{awkcode}
    for (i in pop)
        delete pop[i]
\end{awkcode}

\textbf{\texttt{split} 函数} (\emph{The \texttt{split} Function}). 函数
\verb'split('\textit{str}\verb','\textit{arr}\verb','\textit{fs}\verb')'
将字符串 \textit{str} 切割成一个个字段, 并存储到数组 \textit{arr} 中.
字段的个数作为函数的返回值返回. 第3个参数 \textit{fs} 的字符串值作为字段
的分割符. 如果第3个参数被忽略, 就使用 \verb'FS'. 不管是哪一种情况, 分割
字段的规则等同于输入行的字段分割 (见 \ref{sec:input} 节). 函数
\begin{awkcode}
    split("7/4/76", arr, "/")
\end{awkcode}
将字符串 \verb'7/4/76' 分割成 3 个字段, 使用 \verb'/' 作为分割符; 它将
\verb'7' 存储在 \verb'arr["1"]', 将 \verb'4' 存储在 \verb'arr["2"]',
将 \verb'76' 存储在 \verb'arr["3"]'.

字符串是多功能的数组下标, 但是, 将数值类型的下标作为字符串来索引数组时,
有时候会出现有悖于常理的事情. 因为 \verb'1' 的字符串值与 \verb'"1"' 是相同
的, 所以 \verb'arr[1]' 与 \verb'arr["1"]' 是同一个元素. 但是请注意,
\verb'01' 的字符串值与 \verb'1' 的字符串值并不相同, 并且字符串 \verb'10'
排在字符串 \verb'2' 之前.

\textbf{多维数组} (\emph{Multidimensional Arrays}). Awk 不直接支持多维数组,
但是它利用一维数组来近似模拟多维数组. 虽然你可以写出像 \verb'i, j' 或
\verb's, p, q, r' 这样的多维数组下标, 实际上 awk 会将它们都拼接起来 (下标
之间用一个分隔符分开), 合成一个单独的下标. 举例来说,
\begin{awkcode}
    for (i = 1; i <= 10; i++)
        for (j = 1; j <= 10; j++)
            arr[i, j] = 0
\end{awkcode}
创建了一个具有 100 个元素的数组, 下标具有形式 \verb'1,1', \verb'1,2' 等等.
\marginpar{53}
在 awk 内部, 下标其实是以字符串的形式存储的, 字符串具有形式
\verb'1 SUBSEP 1', \verb'1 SUBSEP 2' 等等. 内建变量 \verb'SUBSEP' 用于分
隔下标的各个构成成分, 它的默认值是 \verb'"\034"', 而不是 逗号, 之所以使用
\verb'"\034"' 是因为这个字符不太可能出现在通常的文本中.

为了测试一个多维下标是否是某个数组的成员, 我们需要将下标用括号括起来, 例如 
\begin{awkcode}
    if ((i,j) in arr) ...
\end{awkcode}
为了遍历一个这样的数组, 可以这样写:
\begin{awkcode}
    for (k in arr) ...
\end{awkcode}
如果需要单独地访问下标的某个成分, 可以使用 \verb'split(k, x, SUBSEP)'.

数组的元素不能再是数组.

\section{用户自定义函数}
\label{sec:user_defined_functions}

除了内建函数, awk 还可以包含用户定义的函数. 定义函数的语句具有形式:
\begin{pattern}
    \verb'function ' \textit{name}\verb'('\textit{parameter-list}\verb')'
        \verb'{' \par
            \indent\indent\stmt \par
        \verb'}'
\end{pattern}
一个函数定义可以出现在任何 \patact 语句可以出现的地方. 于是, awk 程序的 
通常形式就变成了一系列的, 由换行符或分号分开的 \patact 语句与函数定义.

在一个函数定义中, 函数体左花括号后的换行符, 与右花括号前的换行符都是可选
的, 参数列表由一系列的, 逗号分开的变量名构成; 在函数体内部, 这些变量引用
的是函数被调用时传递给它的参数.

函数体可能包含一个 \verb'return' 语句, 用于将程序执行流返回至调用函数 (返
回的时候可以带有一个值). 它具有形式
\begin{pattern}
    \verb'return '\expr
\end{pattern}
\expr 与 \verb'return' 语句都是可选的, 如果没有为 \verb'return' 语句提供 
表达式, 或者最后一句执行的语句不是 \verb'return', 那么返回值就是未定义的.

举例来说, 这个函数计算参数的最大值:
\begin{awkcode}
    function max(m, n) {
        return m > n ? m : n
    }
\end{awkcode}
变量 \verb'm' 与 \verb'n' 属于函数 \verb'max', 它们与程序中其他同名的变量
没有任何关联.
\marginpar{54}

用户定义的函数可以在任何 \patact 语句的任何表达式中使用, 也可以在出现在
任何函数体内. 每一个对函数的使用都叫做一个 \textbf{调用} (\emph{call}).
如果一个用户定义的函数在函数体内调用了它自身, 
我们就说这个函数是 \textbf{递归}
(\emph{recursive}) 的.

举例来说, \verb'max' 函数可以像这样调用:
\begin{awkcode}
    { print max($1, max($2, $3)) } # print maxinum of $1, $2, $3
    function max(m, n) {
        return m > n ? m : n
    }
\end{awkcode}
调用函数时, 函数名与左括号之间不能留有空白.

一个带有参数 \verb'$1' 的函数被调用时 (\verb'$1' 只是一个普通的变量), 函数 
接收到的参数是变量的值的一份拷贝, 所以函数操作的是变量的拷贝, 而不是变量本
身. 这意味着函数不会对函数体外部的变量的值产生影响. (用行话来说, 这样的变
量叫做 ``标量'', ``按值传递'' 给函数) 然而, 当数组作为函数的参数时, 函数 
接收到的参数就不是数组的拷贝, 所以函数可以改变数组的元素, 或往数组增添新
的值 (这叫作 ``按引用传递''). 函数名不能当作参数使用.

重申一遍, 在函数体内部, 参数是局部变量 --- 它们只在函数执行时才存在, 而且 
它们与程序中其他同名的变量没有任何关联. 但是, \textbf{其他所有的变量都是全
局的} (\emph{all other variables are global}); 如果函数体内的某个变量没有
出现在参数列表中, 那么整个程序范围内都可以访问该变量.

这意味着如果函数想要拥有私有的局部变量, 唯一的方法是将该变量包含在参数列
表的末尾. 参数列表中没有实际参数对应的参数都将作为局部变量使用, 初始值为
空值. 虽然这种设计不是非常优雅, 但至少为语言提供了必要的能力. 我们在参数
与局部变量之间多放置几个空格, 以区分它们.

\section{输出}
\label{sec:output}

\print 与 \printf 语句可以用来产生输出. \print 用于产生简单的输出; \printf 
用于产生格式化的输出. 来自 \print 与 \printf 的输出可以被重定向到文件, 管
道与终端. 这两个语句可以混合使用, 输出按照它们产生的顺序出现.

\marginpar{55}
\begin{summary}{输出语句}
    \begin{enumerate}
        \item \print \par
            将 \verb'$0' 打印到标准输出
        \item \print\ \expr\verb',' \expr\verb',' \ldots \par
            打印各个 \expr, \expr 之间由 \verb'OFS' 分开, 由 \verb'ORS' 
            终止
        \item \print\ \expr\verb','\expr\verb','\ldots\ \verb'>'
            \textit{filename}
            \par 输出至文件 \textit{filename}
        \item \print\ \expr\verb','\expr\verb','\ldots\ \verb'>>'
            \textit{filename}
            \par 累加输出到文件 \textit{filename}, 不覆盖之前的内容
        \item \print\ \expr\verb','\expr\verb','\ldots\ \verb'|'
            \textit{command} \par
            输出作为命令 \textit{command} 标准输入
        \item
            \printf\verb'('\fmt\verb','\expr\verb','\expr\verb','\ldots
            \verb')'
        \item
            \printf\verb'('\fmt\verb','\expr\verb','\expr\verb','\ldots
            \verb')' \verb'>' \textit{filename}
        \item
            \printf\verb'('\fmt\verb','\expr\verb','\expr\verb','\ldots
            \verb')' \verb'>>' \textit{filename}
        \item
            \printf\verb'('\fmt\verb','\expr\verb','\expr\verb','\ldots
            \verb')' \verb'|' \textit{command} \par
            \printf 类似于 \print, 但是第 1 个参数规定了输出的格式
        \item \verb'close('\textit{filename}\verb')', \verb'close('
            \textit{command}\verb')' \par
            断开 \print 与 \textit{filename} (或 \textit{command}) 之间 
            的连接
        \item \verb'system('\textit{command}\verb')' \par
            执行 \textit{command}; 函数的返回值是 \textit{command} 的退 
            出状态

        \printf 的参数列表不需要被包围在一对括号中. 但是如果 \print 或 
        \printf 的参数列表中, 有一个表达式含有关系运算符, 那么或者表达式,
        或者 
        参数列表, 必须用一对括号包围. 在非 Unix 系统上可能不提供管道与 
        \verb'system'.
    \end{enumerate}
\end{summary}
\subsection{\print 语句}
\label{subsec:the_print_statement}
\print 语句具有形式:
\begin{pattern}
    \print\ \expr$_1$\verb',' \expr$_2$\verb','...\verb','\expr$_n$ \par
    \print\ \verb'('\expr$_1$\verb',' \expr$_2$\verb','...\verb','
    \expr$_n$\verb')'
\end{pattern}
两种形式都会把表达式的字符串值打印出现, 各个表达式的字符串值之间用输出
字段分隔符分开, 最后跟着输出记录分隔符. 语句 
\begin{awkcode}
    print
\end{awkcode}
是
\begin{awkcode}
    print $0
\end{awkcode}
的缩写形式. 为了打印空白行 (即只含有换行符的行), 可以这样写:
\begin{awkcode}
    print ""
\end{awkcode}
\print 的第 2 种形式将参数列表包围在一对括号当中, 正如 
\begin{awkcode}
    print($1 ":", $2)
\end{awkcode}
\marginpar{56}
两种形式的 \print 都可以产生相同的输出, 但是, 正如我们将会看到的那样, 当
参数含有关系运算符时, 就必须使用括号.

\subsection{输出分隔符}
\label{subsec:output_separators}

输出字段分割符与输出记录分隔符存储在内建变量 \verb'OFS' 与 \verb'ORS' 中.
初始情况下, \verb'OFS' 与 \verb'ORS' 分别被设置成一个空格符与一个换行符,
但它们的值可以在任何时候改变. 举例来说, 下面这个程序打印每一行的第 1 与第 
2 个字段, 字段之间用分号分开, 在每一行的第 2 个字段之后输出两个换行符.
\begin{awkcode}
    BEGIN   { OFS = ":"; ORS = "\n\n" }
            { print $1, $2 }
\end{awkcode}
作为对比,
\begin{awkcode}
    { print $1 $2 }
\end{awkcode}
打印第 1 个与第 2 个字段, 两个字段之间没有输出字段分隔符插入, 这是因为 
\verb'$1 $2' 表示两个字符串的拼接.

\subsection{\texttt{printf} 语句}
\label{subsec:the_printf_statement}

\printf 用于产生格式化的输出. 它与 C 语言中的 \printf 函数很像, 但是 awk 
的 \printf 不支持格式说明符 \verb'*'. 与 \print 一样, 它也有带括号与不带
括号的两种形式:
\begin{pattern}
    \printf\ \fmt\verb','\expr$_1$\verb','\expr$_2$\verb','...\verb','
    \expr$_n$ \par 
    \printf\verb'('\fmt\verb','\expr$_1$\verb','\expr$_2$\verb','
    ...\verb',' \expr$_n$\verb')'
\end{pattern}
参数 \fmt 总是必须的, 它是一个变量, 其字符串值含有字面文本与格式说明符,
字面文本会按照文本的字面值输出, 格式说明符规定了参数列表中的表达式将被如何
格式化地输出, 表 \ref{tbl:printf_format_control_characters} 列出了所有的
格式说明符. 每一个格式说明符都以 \verb'%' 开始, 以转换字符结束, 可能含有
下面三种修饰符:
\marginpar{57}
\begin{tabbing}
    \indent\verb'-' \hspace{4em} \= 表达式在它的域内左对齐 \\
    \indent\textit{width}  \> 为了达到规定的宽度, 必要时填充空格; 前导的 \verb'0'
    表示用零填充 \\
    \indent\verb'.'\textit{prec}   \> 字符串最大宽度, 或十进制数的小数部分的位数
\end{tabbing}
\begin{figure}
        \captionsetup{type=table}
        \caption{\texttt{printf} 格式控制字符}
        \label{tbl:printf_format_control_characters}
\begin{center}
        \begin{tabular}{c|l}
            \hline
            \hline
            字符    & \multicolumn{1}{c}{表达式将被打印成} \\
            \hline
            \verb'c' & ASCII 字符 \\
            \verb'd' & 十进制整数 \\
            \verb'e' & \verb'[-]d.ddddddE[+-]dd' \\
            \verb'f' & \verb'[-]ddd.dddddd' \\
            \verb'g' & 按照 \verb'e' 或 \verb'f' 进行转换, 选择较短的那个,
                无意义的零会被抑制 \\
            \verb'o' & 无符号八进制数 \\
            \verb's' & 字符串 \\
            \verb'x' & 无符号十六进制数 \\
            \verb'%' & 打印一个百分号 \verb'%', 不会有参数被吸收 \\
            \hline
        \end{tabular}
\end{center}
    \end{figure}
    \begin{figure}
        \captionsetup{type=table}
        \caption{\texttt{printf} 格式说明符的例子}
        \label{tbl:examples_of_printf_specifications}
\begin{center}
        \begin{tabular}{l|l|l}
            \hline
            \hline
            \multicolumn{1}{c|}{\texttt{fmt}}  &
            \multicolumn{1}{c|}{\texttt{\$1}}     &
            \multicolumn{1}{c}{\texttt{printf(fmt, \$1)}} \\
            \hline
            \verb'%c'     & \verb'97'       & \verb'a'  \\
            \verb'%d'     & \verb'97.5'     & \verb'97' \\
            \verb'%5d'    & \verb'97.5'     & \verb'   97'  \\
            \verb'%e'     & \verb'97.5'     & \verb'9.750000e+01'   \\
            \verb'%f'     & \verb'97.5'     & \verb'97.500000'      \\
            \verb'%7.2f'  & \verb'97.5'     & \verb'  97.50'        \\
            \verb'%g'     & \verb'97.5'     & \verb'97.5'   \\
            \verb'%.6g'   & \verb'97.5'     & \verb'97.5'   \\
            \verb'%o'     & \verb'97'       & \verb'141'    \\
            \verb'%06o'   & \verb'97'       & \verb'000141' \\
            \verb'%x'     & \verb'97'       & \verb'61' \\
            \verb'%s'          & \verb'January'   & \verb'|January|' \\
            \verb'%10s'        & \verb'January'   & \verb'|   January|' \\
            \verb'%-10s'       & \verb'January'   & \verb'|January   |' \\
            \verb'%.3s'        & \verb'January'   & \verb'|Jan|' \\
            \verb'%10.3s'      & \verb'January'   & \verb'|       Jan|' \\
            \verb'%-10.3s'     & \verb'January'   & \verb'|Jan       |' \\
            \verb'%%'          & \verb'January'   & \verb'%' \\
            \hline
        \end{tabular}
\end{center}
    \end{figure}
表 \ref{tbl:examples_of_printf_specifications} 包含有几个例子, 这些例子 
说明了格式说明符, 数据及其对应的输出. 由 \printf 产生的输出不会在行末自动
插入一个换行符, 除非显式放置了一个换行符.

\subsection{输出到文件}
\label{subsec:output_into_files}

重定向运算符 \verb'>' 与 \verb'>>' 用于将输出重定向到文件, 而不是原来的
标准输出. 下面这个程序将所有输入行的第 1 个与第 3 个字段输出到两个文件
中: 如果第 3 个字段大于 100, 则输出到 \verb'bigpop', 否则的话, 输出到 
\verb'smallpop':
\begin{awkcode}
    $3 > 100    { print $1, $3 > "bigpop" }
    $3 <= 100   { print $1, $3 > "smallpop" }
\end{awkcode}
注意, 文件名必须用双引号括起来; 如果没有双引号的话, \verb'bigpop' 与 
\verb'smallpop' 将被当作未初始化的变量.
\marginpar{58} 文件名也可以是表达式或变量:
\begin{awkcode}
    { print($1, $3) > ($3 > 100 ? "bigpop" : "smallpop") }
\end{awkcode}
上面这个程序做的是同样的工作, 程序 
\begin{awkcode}
    { print > $1 }
\end{awkcode}
将所有的输入行输出到以第 1 个字段命名的文件中.

在 \print 与 \printf 语句中, 如果参数列表中的表达式包含有关系运算符, 那么,
表达式 (或者是参数列表) 需要用括号括起来. 这样做是为了避免由重定向运算符
\verb'>' 带来的歧义. 在程序 
\begin{awkcode}
    { print $1, $2 > $3 }
\end{awkcode}
中, \verb'>' 是重定向运算符, 而不是第 2 个表达式的一部分, 所以程序的功能是
将第 1 个与第 2 个字段输出到以 第 3 个字段命名的文件中. 如果想让
 \verb'>' 成为第 2 个表达式的一部分, 使用括号:
\begin{awkcode}
     { print $1, ($2 > $3) }
\end{awkcode}

还有一点需要注意的是, 重定向运算符只会打开文件一次; 随后的 \print 或
\printf 语句将更多的数据添加到文件的末尾. 重定向运算符 \verb'>' 在打开文件
时先把文件内容清空, 然后再往里写数据. 如果用的是 \verb'>>', 那么文件原来的
内容便会保留下来, 而把新的内容添加到原来内容的末尾.

\subsection{输出到管道}
\label{subsec:output_into_pipe}

在支持管道的系统中, 也可以把输出重定向到管道, 而不仅仅是文件. 语句 
\begin{pattern}
    \verb'print | '\textit{command}
\end{pattern}
导致 \print 的输出以管道的方式传递给 \textit{command}

假设我们想要得到一张表格, 表格的每一行都是大洲的名字及其对应的人口数, 人口
按照从大到小的顺序排列. 下面这个程序为每个大洲计算总人口, 人口数存储在数
组 \verb'pop' 中, \END 动作打印大洲的名字及其对应的人口数, 并把打印的内容
输送给 \verb'sort' 命令\footnote{正确的写法应该是 \texttt{sort
-t'}\texttt{\textbackslash}\texttt{t' -k1rn}}
\begin{awkcode}
    # print continents and populations, sorted by population

    BEGIN { FS = "\t" }
          { pop[$4] += $3 }
    END   { for (c in pop)
              printf("%15s\t%6d\n", c, pop[c]) | "sort -t'\t' +1rn"
          }
\end{awkcode}
程序的输出是
\begin{awkcode}
           Asia	  2173
         Europe	   172
  North America	   340
  South America	   134
\end{awkcode}
\marginpar{59}

管道的另一个用法是把输出重定向至标准错误文件 (Unix 系统); 于是输出将会出现
在用户的终端, 而不是标准输出, 有几种惯用语法可以把输出重定向至标准错误文件:
\begin{awkcode}
    print message | "cat 1>&2"          # redirect cat to stderr
    system("echo '" message "' 1>&2")   # redirect echo to stderr
    print message > "/dev/tty"          # write directly on terminal
\end{awkcode}

虽然我们的大多数例子都把字面字符串包围在双引号中, 其实命令行与文件名可以用
任意的表达式指定. 在涉及到输出重定向的 \print 语句中, 文件或管道通过它们的
名字标识, 也就是说, 上面程序的管道是按字符串的字面值命名的:
\begin{awkcode}
    sort -t'\t' +1rn
\end{awkcode}
通常来说, 在程序运行期间, 文件或管道只被打开一次. 如果文件或管道被显式地
关闭, 而后面又使用到了, 那么它们就会被重新打开.

\subsection{关闭文件与管道}
\label{subsec:closing_files_and_pipes}

语句 \verb'close('\expr\verb')' 关闭一个文件或管道, 文件或管道由 
\expr 指定; \expr 的字符串值必须与最初用于创建文件或管道的字符串值相同.
于是 
\begin{awkcode}
    close("sort -t'\t' +1rn")
\end{awkcode}
关闭上面打开的排序管道.

在同一个程序中, 如果你写了一个文件, 而待会儿想要读取它, 那么就需要调用
\verb'close'. 某一时刻, 同时处于打开状态的文件或管道数量最大值由实现定义.

\section{输入}
\label{sec:input}

为 awk 提供输入数据有若干种方式. 最常见的是把输入数据放在一个文件中, 例如 
文件 \verb'data', 然后再键入
\begin{pattern}
    \indent\verb"awk '"\textit{program}\verb"' data"
\end{pattern}

如果没有指定输入文件, awk 就从它的标准输入读取数据; 所以, 另一种常用的方法
是把另一个程序的输出以管道的方式输送给 awk. 举例来说, 实用程序 \verb'egrep'
从输入行中挑选具有指定正则表达式的行, 虽然 awk 也可以做同样的工作, 但是与前
者相比就慢得多了. 我们可以输入命令
\begin{pattern}
    \indent\verb"egrep 'Asia' countries | awk '"\textit{program}\verb"'"
\end{pattern}
\marginpar{60}
\verb'egrep' 挑出那些含有 \verb'Asia' 的行, 再把这些行输送给 awk 做进一步
的处理.

\subsection{输入分隔符}
\label{subsec:input_separators}

内建变量 \verb'FS' 的默认值是 \verb'" "', 也就是一个空格符. 当 \verb'FS'
具有这个特定值时, 输入字段按照空格和 (或) 制表符分割, 前导的空格与制表符
会被丢弃, 所以下面三行数据中, 其每一行的第1个字段都相同:
\begin{awkcode}
    field1
      field1
        field1      field2
\end{awkcode}
然而, 当 \verb'FS' 是其他值时, 前导的空格与制表符\textbf{不会}被丢弃.

把一个字符串赋值给内建变量 \verb'FS' 就可以改变字段分隔符. 如果字符串的长
度多于一个字符, 那么它会被当成一个正则表达式. 当前输入行中, 与该正则表达
式匹配的最左, 最长, 非空且不重叠的子字符串变成字段分隔符, 举例来说,
\begin{awkcode}
    BEGIN { FS = ",[ \t]*|[ \t]+" }
\end{awkcode}
如果某个子串由一个后面跟着空格或制表符的逗号组成, 或者没有逗号, 只有空格
与制表符, 那么这个子串就是字段分隔符.

如果 \verb'FS' 被设置成单个字符 (除了空格符), 那么这个字符就变成字段分隔
符. 这个约定使得把正则表达式元字符当作字段分隔符来用, 变得很容易:
\begin{awkcode}
    FS = "|"
\end{awkcode}
把 \verb'|' 变成字段分隔符, 但是有一些间接的方法需要注意, 就像 
\begin{awkcode}
    FS = "[ ]"
\end{awkcode}
把字段分隔符设置为一个空格符.

\subsection{多行记录}
\label{subsec:multiline_records}

默认情况下, 记录之间由换行符分隔, 所以术语 ``行'' 与 ``记录'' 在通常情况
下是等价的. 默认的记录分隔符可以通过向内建变量 \verb'RS' 赋予新值来改变,
但是必须按照某种受限的方式来进行. 如果 \verb'RS' 被设置成空值, 正如 
\begin{awkcode}
    BEGIN { RS = "" }
\end{awkcode}
\marginpar{61}
那么记录之间将由一个或多个的空行来分隔, 并且每个记录可以占据多行. 把
\verb'RS' 重新设置成换行符 (\verb'RS = "\n"') 就可以恢复原来的效果.
当记录由多行组成时, 无论 \verb'FS' 是什么值, 换行符总是字段分隔符之一.

处理多行记录的通常方式是使用
\begin{awkcode}
    BEGIN { RS = ""; FS = "\n" }
\end{awkcode}
把记录分隔符设置成一个或多个空白行, 把字段分隔符设置成单个的换行符; 每一
行都是一个单独的字段. 记录的长度是有限制的, 通常是 3000 个字符. 
关于如何处理多行记录, 在第 \ref{chap:data_processing} 章包含更多的讨论.

\subsection{\texttt{getline} 函数}
\label{subsec:the_getline_function}

函数 \verb'getline' 可以从当前输入行, 或文件, 或管道, 读取输入. 
\verb'getline' 抓取下一个记录, 按照通常的方式把记录分割成一个个的字段. 
它会设置 \verb'NF', \verb'NR', 和 \verb'FNR'; 如果存在一个记录, 返回
\verb'1',
若遇到文件末尾, 返回 \verb'0', 发生错误时返回 \verb'-1'
(例如打开文件失败).

表达式 \verb'getline x' 读取下一条记录到变量 \verb'x' 中, 并递增 \verb'NR'
与 \verb'FNR', 不会对记录进行分割, 所以不会设置 \verb'NF'.

表达式 
\begin{awkcode}
    getline <"file"
\end{awkcode}
从文件 \verb'file' 读取输入. 它不会对 \verb'NR' 与 \verb'FNR' 产生影响,
但是会执行字段分割, 并且设置 \verb'NF'.

表达式 
\begin{awkcode}
    getline x <"file"
\end{awkcode}
从 \verb'file' 读取下一条记录, 存到变量 \verb'x' 中. 记录不会被分割成字段,
变量 \verb'NF', \verb'NR', 与 \verb'FNR' 都不会被修改.

表 \ref{tbl:getline_function} 总结了 \verb'getline' 函数的所有形式.
每个表达式的值都是 \verb'getline' 的返回值.

\begin{figure}
    \begin{center}
        \captionsetup{type=table}
        \caption{\texttt{getline} 函数}
        \label{tbl:getline_function}
        \begin{tabular}{l|l}
            \hline
            \hline
            \multicolumn{1}{c|}{表达式}  & \multicolumn{1}{c}{被设置的变量}
                \\
            \hline
            \verb'getline'  & \verb'$0, NF, NR, FNR' \\
            \verb'getline'\ \textit{var} & \textit{var}, \verb'NR, FNR' \\
            \verb'getline <'\textit{file} & \verb'$0, NF' \\
            \verb'getline'\ \textit{var}\ \verb'<'\textit{file} &
                \textit{var} \\
            \textit{cmd}\ \verb'|'\ \verb'getline' & \verb'$0, NF'  \\
            \textit{cmd}\ \verb'|'\ \verb'getline'\ \textit{var} &
                \textit{var} \\
            \hline
        \end{tabular}
    \end{center}
\end{figure}

作为一个例子, 这个程序把它的输入复制到输出, 除了类似下面这样的行:
\begin{awkcode}
    #include "filename"
\end{awkcode}
这些行将被文件 \verb'filename' 的内容所替代.
\begin{awkcode}
    # include - replace #include "f" by contents of file f

    /^#include/ {
        gsub(/"/, "", $2)
        while (getline x <$2 > 0)
            print x
        next
    }
    { print }
\end{awkcode}

\marginpar{62}
也可以把其他命令的输出直接输送给 \verb'getline'. 例如, 语句 
\begin{awkcode}
    while ("who" | getline)
        n++
\end{awkcode}
执行 Unix 命令 \verb'who' (只执行一次), 并将它的输出传递给 \verb'getline'.
\verb'who' 命令的输出是所有已登录用户的用户名, \verb'while' 循环的每一次
迭代都从这个用户名列表里读取一行,
并递增变量 \verb'n', 当循环结束后,
\verb'n' 的值就是已登录用户的人数. 类似的, 表达式 
\begin{awkcode}
    "date" | getline d
\end{awkcode}
把命令 \verb'date' 的输出保存到变量 \verb'd' 中, 于是 \verb'd' 就被设置为
当前日期. 同样, 在非 Unix 系统上可能不支持管道.

在所有的涉及到 \verb'getline' 的情况中, 你必须注意由于文件无法访问而返回的
错误. 写出这样的代码是有可能的
\begin{awkcode}
    while (getline <"file") ...     # dangerous
\end{awkcode}
如果文件不存在的话, 这将是个死循环, 因为对于不存在的文件, \verb'getline'
返回 \verb'-1', 非零值都表示真. 安全的方式是
\begin{awkcode}
    while (getline <"file" > 0) ...     # safe
\end{awkcode}
这个循环只有在 \verb'getline' 返回 \texttt{1} 的情况下才会执行.

\subsection{命令行变量赋值}
\label{subsec:command_line_variable_assignments}

\marginpar{63}
正如我们之前看到过的那样, awk 命令行具有多种形式:
\begin{pattern}
    \indent\verb"awk '"\textit{program}\verb"' f1 f2 ..." \par
    \indent\verb"awk -f "\textit{progfile}\verb" f1 f2 ..." \par
    \indent\verb"awk -F"\textit{sep}\verb" '"\textit{program}\verb"'"
    \verb"f1 f2 ..." \par
    \indent\verb"awk -F"\textit{sep}\verb" -f "\textit{progfile}
        \verb" f1 f2 ..." \par
\end{pattern}
在上面的命令行中, \verb'f1, f2' 等变量是命令行参数, 通常代表文件名. 
如果一个文件名具有形式 \textit{var}\verb'='\textit{text}, 那这就表示赋
值语句, 把 \textit{text} 赋值给 \textit{var}, 当这个参数被当作文件来访问时,
执行赋值动作. 这种类型的赋值语句允许程序在读文件之前或之后改变变量的值.

\subsection{命令行参数}
\label{subsec:command_line_arguments}

命令行参数可以通常 awk 的内建数组 \verb'ARGV' 来访问, 内建变量 \verb'ARGC'
的值是参数的个数再加 1. 对于命令行
\begin{awkcode}
    awk -f progfile a v=1 b
\end{awkcode}
\verb'ARGC' 的值是 \verb'4', \verb'ARGV[0]' 含有 \verb'awk', \verb'ARGV[1]'
含有 \verb'a', \verb'ARGV[2]' 含有 \verb'v=1', \verb'ARGV[3]' 含有
\verb'b'. \verb'ARGC' 之所以会比参数的个数多1, 是因为命令的名字 \verb'awk'
也被当作参数之一, 存放在索引为 0 的位置, 就像 C 程序那样. 然而, 如果 awk 
程序在命令行中出现, 那它就不会被当作参数, 对 \verb'-f '\textit{filename}
或任意的 \verb'-F' 选项同样如此. 例如, 对于命令行
\begin{awkcode}
    awk -F'\t' '$3 > 100' countries
\end{awkcode}
\verb'ARGC' 的值是 \verb'2', \verb'ARGV[1]' 的值是 \verb'countries'.

下面这个程序回射它的命令行参数:
\begin{awkcode}
    # echo - print command-line arguments

    BEGIN {
        for (i = 1; i < ARGC; i++)
            printf "%s ", ARGV[i]
        printf "\n"
    }
\end{awkcode}
请注意在 \verb'BEGIN' 动作里所发生的一切: 因为没有其他的 \patact 语句, 
所以参数不会被当作文件名, 也没有输入被读取.

另一个使用命令行参数的例子是 \verb'seq', 它可以产生一个整数序列:
\begin{awkcode}
    # seq - print sequences of integers
    # input:  arguments q, p q, or p q r;  q >= p; r > 0
    # output: integers 1 to q, p to q, or p to q in steps of r

    BEGIN {
        if (ARGC == 2)
            for (i = 1; i <= ARGV[1]; i++)
                print i
        else if (ARGC == 3)
            for (i = ARGV[1]; i <= ARGV[2]; i++)
                print i
        else if (ARGC == 4)
            for (i = ARGV[1]; i <= ARGV[2]; i += ARGV[3])
                print i
    }
\end{awkcode}
命令 
\marginpar{64}
\begin{awkcode}
    awk -f seq 10 
    awk -f seq 1 10 
    awk -f seq 1 10 1
\end{awkcode}
都是生成 1 到 10 这十个整数.

\verb'ARGV' 里面的参数可以修改或添加, \verb'ARGC' 可能会随之改变. 每当有一 
个输入文件结束, awk 把 \verb'ARGV' 的下一个非空元素 (直到 \verb'ARGC - 1'
的当前值) 看作是下一个输入文件名. 于是, 把 \verb'ARGV' 的一个元素设置为
空, 意味着 awk 不会把它看作是输入文件. 名字 ``\verb"-"'' 可以当作标准输入
来用.

\section{与其他程序的交互}
\label{sec:interaction_with_other_programs}

这一节描述几种 awk 程序与其他命令合作的方式. 主要针对 Unix 系统, 在非 
Unix 系统上, 这里的例子可能无法工作或表现出不同的行为.

\subsection{\texttt{system} 函数}
\label{subsec:the_system_function}

内建函数 \verb'system('\textit{expression}\verb')' 用于执行命令, 命令由 
\textit{expression} 给出, \verb'system' 的返回值就是命令的退出状态.

例如, 我们可以重新实现 \ref{sec:input} 节提到的文件包含程序, 就像这样
\begin{awkcode}
    $1 == "#include" { gsub(/"/, "", $2); system("cat " $2); next }
                     { print }
\end{awkcode}
如果第一个字段是 \verb'#include', 那么双引号就会被移除, 然后 Unix 命令 
\verb'cat' 打印以第 2 个字段命名的文件. 其他输入行被原样复制.

\subsection{用 AWK 制作 Shell 命令}
\label{subsec:making_a_shell_command_from_an_awk_program}

\marginpar{65}
到目前为止的所有例子, awk 程序都是写在一个文件中, 然后利用 \verb'-f'
选项读取, 或者是出现在命令行中, 用一对单引号括起来, 就像:
\begin{awkcode}
    awk '{ print $1 }' ...
\end{awkcode}
Awk 用到了许多 Shell 也同样会用到的字符, 例如 \verb'$' 与 \verb'"',
把 awk 程序包围在一对单引号中可以确保 Shell 把程序原封不动地传递给 awk.

上面提到的两种执行 awk 程序的方法都要求用户自己打一些字. 为了降低打字的
工作量, 我们想要把命令与程序都写到一个可执行文件中, 通过键入文件名来执行
程序. 假设我们想要写一个命令 \verb'field1', 用于打印每个输入行的第一个
字段, 其实这非常容易, 我们把
\begin{awkcode}
    awk '{ print $1 }' $*
\end{awkcode}
写到一个文件 \verb'field1' 中, 为了让它成为一个可执行文件, 我们必须执行
Unix 命令:
\begin{awkcode}
    chmod +x field1
\end{awkcode}
现在, 如果我们想要打印某些文件的每行的第 1 个字段, 只要键入
\begin{pattern}
    \indent\texttt{field1 } \textit{filenames} ...
\end{pattern}

现在我们考虑实现一个更加通用的 \verb'field', 程序可以从每个输入行中
打印任意组合的字段, 换句话说, 命令 
\begin{pattern}
    \indent\verb'field '\textit{n}$_1$\ \textit{n}$_2$\ ...\ \textit{file}$_1$\ \textit{file}
        $_2$\ ...
\end{pattern}
按照特定的顺序打印特定的字段. 但是我们如何获取 \textit{n}$_i$ 的值, 又如何区分
\textit{n}$_i$ 与文件名参数?

对于采用 Shell 编程的人来说, 解决上面提到的两个问题有多种方式, 如果使用 
awk 的话, 最简单的方式是扫描内建数组 \verb'ARGV', 获取 \textit{n}$_i$
的值之后, 把数组中对应的位置清零, 这样它们就不会被当成文件名:
\marginpar{66}
\begin{awkcode}
    # field - print named fields of each input line
    #   usage:  field n n n ... file file file ...

    awk '
    BEGIN {
        for (i = 1; ARGV[i] ~ /^[0-9]+$/; i++) { # collect numbers
            fld[++nf] = ARGV[i]
            ARGV[i] = ""
        }
        if (i >= ARGC)   # no file names so force stdin
            ARGV[ARGC++] = "-"
    }
    {   for (i = 1; i <= nf; i++)
            printf("%s%s", $fld[i], i < nf ? " " : "\n")
    }
    ' $*
\end{awkcode}
这个程序可以处理标准输入, 也可以处理文件名参数列表, 并且字段的顺序与数量
都可以是任意的.

\section{小结}
\label{sec:the_awk_language_summary}

正如我刚开始说的那样, 这一章很长, 讨论了许多细节, 如果读者是逐字逐句地读到
这里, 那确实是非常专注. 到后面读者就会发现, 时不时地就需要回到这章
再看一下某些小节, 有时候是为了精确地了解程序如何工作, 有时候则是因为后面
的章节出现了一些之前没有试过的构造方法.

和其他语言一样, awk 也需要通过经验与实践来学习, 所以我们鼓励读者开始自己写
程序. 程序不需要有多么的庞大或复杂 --- 你可以通过仅仅几行代码, 来了解
语言的某些特性如何工作, 或测试程序的某些关键点, 你也可以自己手动输入数据
来查看程序的运行效果.

\subsection*{参考资料}
\label{subsec:bibliographic_notes}

\textit{The C Programming Language} (Brian Kernighan 与 Dennis Ritchie 著,
Prentice-Hall 1978 年出版) 对 C 程序设计语言进行了完整地描述. 关于如何 
使用 Unix 系统有大量的资料可供参考; \textit{The Unix Programming 
Environment} (Brian Kernighan 与 Rob Pike 著, Prentice-Hall 1984 年出版)
包含一个扩展讨论, 讨论的是如何创建包含 awk 的 shell 程序.
